[utils] adding `HistoryTracker` module (#6807)

This commit adds History Tracker feature and its CLI support. This
feature records history of different events as the Thread network
operates (e.g., history of RX and TX IPv6 messages or network info
changes).

Recorded entries are timestamped. When the history list is read, the
timestamps are given as the entry age relative to the time the list
is being read. For example in CLI a timestamp can be shown as
`02:31:50.628 ago` indicating the entry was recorded 2 hours, 31 min,
50 sec, and 628 msec ago. Number of days is added for events that are
older than 24 hours, e.g., `31 days 03:00:23.931 ago`. Timestamps use
millisecond accuracy and are tacked up to 49 days. If an event is
older than 49 days, the entry is still tracked in the list but the
timestamp is shown as old or `more than 49 days ago`.

The `HistoryTracker` currently maintains 3 lists. The Network Info
history tracks changes to Device Role, Mode, RLOC16 and Partition ID.
The RX/TX history list records information about the received/sent
IPv6 messages:
- Message type (UDP, TCP, ICMP6 (and its subtype), etc.)
- Source and destination IPv6 addresses and port numbers
- IPv6 payload length
- The message checksum (for UDP, TCP, or ICMP6).
- Whether or not the link-layer security was used
- Message priority: low, norm, high, net (for control messages)
- Short address (RLOC16) of neighbor who send/received the msg
- Received Signal Strength (in dBm) for RX only
- Radio link info (15.4/TREL) on which msg was sent/received
  (useful when `OPENTHREAD_CONFIG_MULTI_RADIO` is enabled)

Config `HISTORY_TRACKER_EXCLUDE_THREAD_CONTROL_MESSAGES` can be used
to configure `HistoryTracker` to exclude Thread Control message
(e.g., MLE, TMF) from TX and RX history.

The number of entries recorded for each history list is configurable
through a set of OpenThread config options, e.g., number of entries
in Network Info history list is specified by OpenThread config option
`OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE`. The
`HistoryTracker` will keep the most recent entries overwriting oldest
ones when the list gets full.

This commit also adds support for `HistoryTracker` in CLI. The CLI
commands provide two style for printing the history information: A
table format (more human-readable) and list style (better suited for
parsing by machine/code). `README_HISTORY.md` is added to document
the commands and the info provided by each history list entry.

This commit also adds `test_history_tracker.py` test-case which
covers the behavior of `HistoryTracker`.
diff --git a/.github/workflows/simulation-1.1.yml b/.github/workflows/simulation-1.1.yml
index 0cff6d0..5889015 100644
--- a/.github/workflows/simulation-1.1.yml
+++ b/.github/workflows/simulation-1.1.yml
@@ -62,6 +62,7 @@
         export ASAN_SYMBOLIZER_PATH=`which llvm-symbolizer`
         export ASAN_OPTIONS=symbolize=1
         export DISTCHECK_CONFIGURE_FLAGS= CPPFLAGS=-DOPENTHREAD_SIMULATION_VIRTUAL_TIME=1
+        export DISTCHECK_BUILD=1
         ./bootstrap
         VERBOSE=1 make -f examples/Makefile-simulation distcheck
 
diff --git a/Android.mk b/Android.mk
index baf2ea2..af425f3 100644
--- a/Android.mk
+++ b/Android.mk
@@ -178,6 +178,7 @@
     src/core/api/entropy_api.cpp                                    \
     src/core/api/error_api.cpp                                      \
     src/core/api/heap_api.cpp                                       \
+    src/core/api/history_tracker_api.cpp                            \
     src/core/api/icmp6_api.cpp                                      \
     src/core/api/instance_api.cpp                                   \
     src/core/api/ip6_api.cpp                                        \
@@ -342,6 +343,7 @@
     src/core/utils/child_supervision.cpp                            \
     src/core/utils/flash.cpp                                        \
     src/core/utils/heap.cpp                                         \
+    src/core/utils/history_tracker.cpp                              \
     src/core/utils/jam_detector.cpp                                 \
     src/core/utils/lookup_table.cpp                                 \
     src/core/utils/otns.cpp                                         \
@@ -498,6 +500,7 @@
     src/cli/cli_coap_secure.cpp               \
     src/cli/cli_commissioner.cpp              \
     src/cli/cli_dataset.cpp                   \
+    src/cli/cli_history.cpp                   \
     src/cli/cli_joiner.cpp                    \
     src/cli/cli_network_data.cpp              \
     src/cli/cli_srp_client.cpp                \
diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake
index d9dcb08..1d6230a 100644
--- a/etc/cmake/options.cmake
+++ b/etc/cmake/options.cmake
@@ -211,6 +211,11 @@
     target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE=1")
 endif()
 
+option(OT_HISTORY_TRACKER "enable history tracker support")
+if(OT_HISTORY_TRACKER)
+    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE=1")
+endif()
+
 option(OT_IP6_FRAGM "enable ipv6 fragmentation support")
 if(OT_IP6_FRAGM)
     target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE=1")
diff --git a/examples/Makefile-simulation b/examples/Makefile-simulation
index 39e61f1..8bdcb3f 100644
--- a/examples/Makefile-simulation
+++ b/examples/Makefile-simulation
@@ -52,6 +52,7 @@
 DNS_CLIENT                     ?= 1
 DNSSD_SERVER                   ?= 1
 ECDSA                          ?= 1
+HISTORY_TRACKER                ?= 1
 IP6_FRAGM                      ?= 1
 JAM_DETECTION                  ?= 1
 JOINER                         ?= 1
diff --git a/examples/README.md b/examples/README.md
index ea9b0c3..85db818 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -37,6 +37,7 @@
 | ECDSA | OT_ECDSA | Enables support for Elliptic Curve Digital Signature Algorithm. Enable this switch if ECDSA digital signature is used by application. |
 | EXTERNAL_HEAP | OT_EXTERNAL_HEAP | Enables support for external heap. Enable this switch if the platform uses its own heap. Make sure to specify the external heap Calloc and Free functions to be used by the OpenThread stack. |
 | FULL_LOGS | OT_FULL_LOGS | Enables all log levels and regions. This switch sets the log level to OT_LOG_LEVEL_DEBG and turns on all region flags. See [Logging guide](https://openthread.io/guides/build/logs) to learn more. |
+| HISTORY_TRACKER | OT_HISTORY_TRACKER | Enables support for History Tracker. |
 | IP6_FRAGM | OT_IP6_FRAGM | Enables support for IPv6 fragmentation. |
 | JAM_DETECTION | OT_JAM_DETECTION | Enables support for [Jam Detection](https://openthread.io/guides/build/features/jam-detection). Enable this switch if a device requires the ability to detect signal jamming on a specific channel. |
 | JOINER | OT_JOINER | Enables [support for Joiner](https://openthread.io/reference/group/api-joiner). Enable this switch on a device that has to be commissioned to join the network. |
diff --git a/examples/common-switches.mk b/examples/common-switches.mk
index 2040530..a3ae15b 100644
--- a/examples/common-switches.mk
+++ b/examples/common-switches.mk
@@ -55,6 +55,7 @@
 DYNAMIC_LOG_LEVEL         ?= 0
 ECDSA                     ?= 0
 EXTERNAL_HEAP             ?= 0
+HISTORY_TRACKER           ?= 0
 IP6_FRAGM                 ?= 0
 JAM_DETECTION             ?= 0
 JOINER                    ?= 0
@@ -211,6 +212,10 @@
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE=1
 endif
 
+ifeq ($(HISTORY_TRACKER),1)
+COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE=1
+endif
+
 ifeq ($(IP6_FRAGM),1)
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE=1
 endif
diff --git a/include/Makefile.am b/include/Makefile.am
index 9cf92de..6feb446 100644
--- a/include/Makefile.am
+++ b/include/Makefile.am
@@ -58,6 +58,7 @@
     openthread/entropy.h                  \
     openthread/error.h                    \
     openthread/heap.h                     \
+    openthread/history_tracker.h          \
     openthread/icmp6.h                    \
     openthread/instance.h                 \
     openthread/ip6.h                      \
diff --git a/include/openthread/BUILD.gn b/include/openthread/BUILD.gn
index 1bc5d55..6a365fd 100644
--- a/include/openthread/BUILD.gn
+++ b/include/openthread/BUILD.gn
@@ -79,6 +79,7 @@
     "entropy.h",
     "error.h",
     "heap.h",
+    "history_tracker.h",
     "icmp6.h",
     "instance.h",
     "ip6.h",
diff --git a/include/openthread/history_tracker.h b/include/openthread/history_tracker.h
new file mode 100644
index 0000000..a9d1608
--- /dev/null
+++ b/include/openthread/history_tracker.h
@@ -0,0 +1,209 @@
+/*
+ *  Copyright (c) 2021, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ */
+
+#ifndef OPENTHREAD_HISTORY_TRACKER_H_
+#define OPENTHREAD_HISTORY_TRACKER_H_
+
+#include <openthread/instance.h>
+#include <openthread/ip6.h>
+#include <openthread/thread.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief
+ *   This header defines the public API for History Tracker.
+ *
+ *   History Tracker module records history of different events (e.g. RX and TX messages or network info changes, etc.)
+ *   as the Thread network operates. All tracked entries are timestamped.
+ *
+ *   The functions in this module are available when `OPENTHREAD_CONFIG_HISTOR_TRACKER_ENABLE` is enabled.
+ *
+ */
+
+/**
+ * This constant specifies the maximum age of entries which is 49 days (in msec).
+ *
+ * Entries older than the max age will give this value as their age.
+ *
+ */
+#define OT_HISTORY_TRACKER_MAX_AGE (49 * 24 * 60 * 60 * 1000u)
+
+/**
+ * This type represents an iterator to iterate through a history list.
+ *
+ * The fields in this type are opaque (intended for use by OpenThread core) and therefore should not be accessed/used
+ * by caller.
+ *
+ * Before using an iterator, it MUST be initialized using `otHistoryTrackerInitIterator()`,
+ *
+ */
+typedef struct otHistoryTrackerIterator
+{
+    uint32_t mData32;
+    uint16_t mData16;
+} otHistoryTrackerIterator;
+
+/**
+ * This structure represents Thread network info.
+ *
+ */
+typedef struct otHistoryTrackerNetworkInfo
+{
+    otDeviceRole     mRole;        ///< Device Role.
+    otLinkModeConfig mMode;        ///< Device Mode.
+    uint16_t         mRloc16;      ///< Device RLOC16.
+    uint32_t         mPartitionId; ///< Partition ID (valid when attached).
+} otHistoryTrackerNetworkInfo;
+
+/**
+ * Constants representing message priority used in `otHistoryTrackerMessageInfo` struct.
+ *
+ */
+enum
+{
+    OT_HISTORY_TRACKER_MSG_PRIORITY_LOW    = OT_MESSAGE_PRIORITY_LOW,      ///< Low priority level.
+    OT_HISTORY_TRACKER_MSG_PRIORITY_NORMAL = OT_MESSAGE_PRIORITY_NORMAL,   ///< Normal priority level.
+    OT_HISTORY_TRACKER_MSG_PRIORITY_HIGH   = OT_MESSAGE_PRIORITY_HIGH,     ///< High priority level.
+    OT_HISTORY_TRACKER_MSG_PRIORITY_NET    = OT_MESSAGE_PRIORITY_HIGH + 1, ///< Network Control priority level.
+};
+
+/**
+ * This structure represents a RX/TX IPv6 message info.
+ *
+ * Some of the fields in this struct are applicable to a RX message or a TX message only, e.g., `mAveRxRss` is the
+ * average RSS of all fragment frames that form a received message and is only applicable for a RX message.
+ *
+ */
+typedef struct otHistoryTrackerMessageInfo
+{
+    uint16_t   mPayloadLength;       ///< IPv6 payload length (exclude IP6 header itself).
+    uint16_t   mNeighborRloc16;      ///< RLOC16 of neighbor which sent/received the msg (`0xfffe` if no RLOC16).
+    otSockAddr mSource;              ///< Source IPv6 address and port (if UDP/TCP)
+    otSockAddr mDestination;         ///< Destination IPv6 address and port (if UDP/TCP).
+    uint16_t   mChecksum;            ///< Message checksum (valid only for UDP/TCP/ICMP6).
+    uint8_t    mIpProto;             ///< IP Protocol number (`OT_IP6_PROTO_*` enumeration).
+    uint8_t    mIcmp6Type;           ///< ICMP6 type if msg is ICMP6, zero otherwise (`OT_ICMP6_TYPE_*` enumeration).
+    int8_t     mAveRxRss;            ///< RSS of received message or OT_RADIO_INVALI_RSSI if not known.
+    bool       mLinkSecurity : 1;    ///< Indicates whether msg used link security.
+    bool       mTxSuccess : 1;       ///< Indicates TX success (e.g., ack received). Applicable for TX msg only.
+    uint8_t    mPriority : 2;        ///< Message priority (`OT_HISTORY_TRACKER_MSG_PRIORITY_*` enumeration).
+    bool       mRadioIeee802154 : 1; ///< Indicates whether msg was sent/received over a 15.4 radio link.
+    bool       mRadioTrelUdp6 : 1;   ///< Indicates whether msg was sent/received over a TREL radio link.
+} otHistoryTrackerMessageInfo;
+
+/**
+ * This function initializes an `otHistoryTrackerIterator`.
+ *
+ * An iterator MUST be initialized before it is used.
+ *
+ * An iterator can be initialized again to start from the beginning of the list.
+ *
+ * When iterating over entries in a list, to ensure the entry ages are consistent, the age is given relative to the
+ * time the iterator was initialized, i.e., the entry age is provided as the duration (in milliseconds) from the event
+ * (when entry was recorded) to the iterator initialization time.
+ *
+ * @param[in] aIterator  A pointer to the iterator to initialize (MUST NOT be NULL).
+ *
+ */
+void otHistoryTrackerInitIterator(otHistoryTrackerIterator *aIterator);
+
+/**
+ * This function iterates over the entries in the network info history list.
+ *
+ * @param[in]    aInstance   A pointer to the OpenThread instance.
+ * @param[inout] aIterator   A pointer to an iterator. MUST be initialized or the behavior is undefined.
+ * @param[out]   aEntryAge   A pointer to a variable to output the entry's age. MUST NOT be NULL.
+ *                           Age is provided as the duration (in milliseconds) from when entry was recorded to
+ *                           @p aIterator initialization time. It is set to `OT_HISTORY_TRACKER_MAX_AGE` for entries
+ *                           older than max age.
+ *
+ * @returns A pointer to `otHistoryTrackerNetworkInfo` entry or `NULL` if no more entries in the list.
+ *
+ */
+const otHistoryTrackerNetworkInfo *otHistoryTrackerIterateNetInfoHistory(otInstance *              aInstance,
+                                                                         otHistoryTrackerIterator *aIterator,
+                                                                         uint32_t *                aEntryAge);
+
+/**
+ * This function iterates over the entries in the RX message history list.
+ *
+ * @param[in]    aInstance   A pointer to the OpenThread instance.
+ * @param[inout] aIterator   A pointer to an iterator. MUST be initialized or the behavior is undefined.
+ * @param[out]   aEntryAge   A pointer to a variable to output the entry's age. MUST NOT be NULL.
+ *                           Age is provided as the duration (in milliseconds) from when entry was recorded to
+ *                           @p aIterator initialization time. It is set to `OT_HISTORY_TRACKER_MAX_AGE` for entries
+ *                           older than max age.
+ *
+ * @returns The `otHistoryTrackerMessageInfo` entry or `NULL` if no more entries in the list.
+ *
+ */
+const otHistoryTrackerMessageInfo *otHistoryTrackerIterateRxHistory(otInstance *              aInstance,
+                                                                    otHistoryTrackerIterator *aIterator,
+                                                                    uint32_t *                aEntryAge);
+
+/**
+ * This function iterates over the entries in the TX message history list.
+ *
+ * @param[in]    aInstance   A pointer to the OpenThread instance.
+ * @param[inout] aIterator   A pointer to an iterator. MUST be initialized or the behavior is undefined.
+ * @param[out]   aEntryAge   A pointer to a variable to output the entry's age. MUST NOT be NULL.
+ *                           Age is provided as the duration (in milliseconds) from when entry was recorded to
+ *                           @p aIterator initialization time. It is set to `OT_HISTORY_TRACKER_MAX_AGE` for entries
+ *                           older than max age.
+ *
+ * @returns The `otHistoryTrackerMessageInfo` entry or `NULL` if no more entries in the list.
+ *
+ */
+const otHistoryTrackerMessageInfo *otHistoryTrackerIterateTxHistory(otInstance *              aInstance,
+                                                                    otHistoryTrackerIterator *aIterator,
+                                                                    uint32_t *                aEntryAge);
+
+#define OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE 21 ///< Recommended size for string representation of an entry age.
+
+/**
+ * This function converts a given entry age to a human-readable string.
+ *
+ * The entry age string follows the format "<hh>:<mm>:<ss>.<mmmm>" for hours, minutes, seconds and millisecond (if
+ * shorter than one day) or "<dd> days <hh>:<mm>:<ss>.<mmmm>" (if longer than one day).
+ *
+ * If the resulting string does not fit in @p aBuffer (within its @p aSize characters), the string will be truncated
+ * but the outputted string is always null-terminated.
+ *
+ * @param[in]  aEntryAge The entry age (duration in msec).
+ * @param[out] aBuffer   A pointer to a char array to output the string (MUST NOT be NULL).
+ * @param[in]  aSize     The size of @p aBuffer (in bytes). Recommended to use `OT_IP6_ADDRESS_STRING_SIZE`.
+ *
+ */
+void otHistoryTrackerEntryAgeToString(uint32_t aEntryAge, char *aBuffer, uint16_t aSize);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // OPENTHREAD_HISTORY_TRACKER_H_
diff --git a/include/openthread/instance.h b/include/openthread/instance.h
index 9f9ea25..2a4d21d 100644
--- a/include/openthread/instance.h
+++ b/include/openthread/instance.h
@@ -53,7 +53,7 @@
  * @note This number versions both OpenThread platform and user APIs.
  *
  */
-#define OPENTHREAD_API_VERSION (150)
+#define OPENTHREAD_API_VERSION (151)
 
 /**
  * @addtogroup api-instance
diff --git a/include/openthread/ip6.h b/include/openthread/ip6.h
index 33bf301..cd8ad53 100644
--- a/include/openthread/ip6.h
+++ b/include/openthread/ip6.h
@@ -227,6 +227,23 @@
 } otMessageInfo;
 
 /**
+ * Internet Protocol Numbers.
+ *
+ */
+enum
+{
+    OT_IP6_PROTO_HOP_OPTS = 0,  ///< IPv6 Hop-by-Hop Option
+    OT_IP6_PROTO_TCP      = 6,  ///< Transmission Control Protocol
+    OT_IP6_PROTO_UDP      = 17, ///< User Datagram
+    OT_IP6_PROTO_IP6      = 41, ///< IPv6 encapsulation
+    OT_IP6_PROTO_ROUTING  = 43, ///< Routing Header for IPv6
+    OT_IP6_PROTO_FRAGMENT = 44, ///< Fragment Header for IPv6
+    OT_IP6_PROTO_ICMP6    = 58, ///< ICMP for IPv6
+    OT_IP6_PROTO_NONE     = 59, ///< No Next Header for IPv6
+    OT_IP6_PROTO_DST_OPTS = 60, ///< Destination Options for IPv6
+};
+
+/**
  * This function brings up/down the IPv6 interface.
  *
  * Call this function to enable/disable IPv6 communication.
@@ -798,6 +815,16 @@
 otError otIp6SetMeshLocalIid(otInstance *aInstance, const otIp6InterfaceIdentifier *aIid);
 
 /**
+ * This function converts a given IP protocol number to a human-readable string.
+ *
+ * @param[in] aIpProto   An IP protocol number (`OT_IP6_PROTO_*` enumeration).
+ *
+ * @returns A string representing @p aIpProto.
+ *
+ */
+const char *otIp6ProtoToString(uint8_t aIpProto);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/thread.h b/include/openthread/thread.h
index b3968ff..6f87a7b 100644
--- a/include/openthread/thread.h
+++ b/include/openthread/thread.h
@@ -710,6 +710,16 @@
 otDeviceRole otThreadGetDeviceRole(otInstance *aInstance);
 
 /**
+ * Convert the device role to human-readable string.
+ *
+ * @param[in] aRole   The device role to convert.
+ *
+ * @returns A string representing @p aRole.
+ *
+ */
+const char *otThreadDeviceRoleToString(otDeviceRole aRole);
+
+/**
  * This function get the Thread Leader Data.
  *
  * @param[in]   aInstance    A pointer to an OpenThread instance.
diff --git a/script/check-scan-build b/script/check-scan-build
index 7f4d161..ccbc52e 100755
--- a/script/check-scan-build
+++ b/script/check-scan-build
@@ -50,6 +50,7 @@
         "-DOPENTHREAD_CONFIG_DNS_CLIENT_ENABLE=1"
         "-DOPENTHREAD_CONFIG_ECDSA_ENABLE=1"
         "-DOPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE=1"
+        "-DOPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE=1"
         "-DOPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE=1"
         "-DOPENTHREAD_CONFIG_IP6_SLAAC_ENABLE=1"
         "-DOPENTHREAD_CONFIG_JAM_DETECTION_ENABLE=1"
diff --git a/script/check-simulation-build-autotools b/script/check-simulation-build-autotools
index 0f7fbc9..4b17fe6 100755
--- a/script/check-simulation-build-autotools
+++ b/script/check-simulation-build-autotools
@@ -55,6 +55,7 @@
         "-DOPENTHREAD_CONFIG_DNS_CLIENT_ENABLE=1"
         "-DOPENTHREAD_CONFIG_ECDSA_ENABLE=1"
         "-DOPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE=1"
+        "-DOPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE=1"
         "-DOPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE=1"
         "-DOPENTHREAD_CONFIG_IP6_SLAAC_ENABLE=1"
         "-DOPENTHREAD_CONFIG_JAM_DETECTION_ENABLE=1"
diff --git a/script/cmake-build b/script/cmake-build
index 8f7793b..8e1401f 100755
--- a/script/cmake-build
+++ b/script/cmake-build
@@ -82,6 +82,7 @@
     "-DOT_DIAGNOSTIC=ON"
     "-DOT_DNS_CLIENT=ON"
     "-DOT_ECDSA=ON"
+    "-DOT_HISTORY_TRACKER=ON"
     "-DOT_IP6_FRAGM=ON"
     "-DOT_JAM_DETECTION=ON"
     "-DOT_JOINER=ON"
diff --git a/script/make-pretty b/script/make-pretty
index 8f4ec67..9c07fcf 100755
--- a/script/make-pretty
+++ b/script/make-pretty
@@ -99,6 +99,7 @@
     '-DOT_DUA=ON'
     '-DOT_MLR=ON'
     '-DOT_ECDSA=ON'
+    '-DOT_HISTORY_TRACKER=ON'
     '-DOT_IP6_FRAGM=ON'
     '-DOT_JAM_DETECTION=ON'
     '-DOT_JOINER=ON'
diff --git a/script/test b/script/test
index c0f15bf..3f042be 100755
--- a/script/test
+++ b/script/test
@@ -59,6 +59,7 @@
         "-DOT_DNS_CLIENT=ON"
         "-DOT_DNSSD_SERVER=ON"
         "-DOT_ECDSA=ON"
+        "-DOT_HISTORY_TRACKER=ON"
         "-DOT_MESSAGE_USE_HEAP=ON"
         "-DOT_NETDATA_PUBLISHER=ON"
         "-DOT_PING_SENDER=ON"
diff --git a/src/cli/BUILD.gn b/src/cli/BUILD.gn
index ce2314c..4ead712 100644
--- a/src/cli/BUILD.gn
+++ b/src/cli/BUILD.gn
@@ -39,6 +39,8 @@
   "cli_config.h",
   "cli_dataset.cpp",
   "cli_dataset.hpp",
+  "cli_history.cpp",
+  "cli_history.hpp",
   "cli_joiner.cpp",
   "cli_joiner.hpp",
   "cli_network_data.cpp",
diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt
index 77a01ce..aacef8e 100644
--- a/src/cli/CMakeLists.txt
+++ b/src/cli/CMakeLists.txt
@@ -37,6 +37,7 @@
     cli_coap_secure.cpp
     cli_commissioner.cpp
     cli_dataset.cpp
+    cli_history.cpp
     cli_joiner.cpp
     cli_network_data.cpp
     cli_srp_client.cpp
diff --git a/src/cli/Makefile.am b/src/cli/Makefile.am
index 3fdc14e..7d8b031 100644
--- a/src/cli/Makefile.am
+++ b/src/cli/Makefile.am
@@ -145,6 +145,7 @@
     cli_coap_secure.cpp               \
     cli_commissioner.cpp              \
     cli_dataset.cpp                   \
+    cli_history.cpp                   \
     cli_joiner.cpp                    \
     cli_network_data.cpp              \
     cli_srp_client.cpp                \
@@ -168,6 +169,7 @@
     cli_commissioner.hpp              \
     cli_config.h                      \
     cli_dataset.hpp                   \
+    cli_history.hpp                   \
     cli_joiner.hpp                    \
     cli_network_data.hpp              \
     cli_srp_client.hpp                \
diff --git a/src/cli/README.md b/src/cli/README.md
index 56bee0e..de08bae 100644
--- a/src/cli/README.md
+++ b/src/cli/README.md
@@ -52,6 +52,7 @@
 - [factoryreset](#factoryreset)
 - [fake](#fake)
 - [fem](#fem)
+- [history](README_HISTORY.md)
 - [ifconfig](#ifconfig)
 - [ipaddr](#ipaddr)
 - [ipmaddr](#ipmaddr)
diff --git a/src/cli/README_HISTORY.md b/src/cli/README_HISTORY.md
new file mode 100644
index 0000000..be50365
--- /dev/null
+++ b/src/cli/README_HISTORY.md
@@ -0,0 +1,340 @@
+# OpenThread CLI - History Tracker
+
+History Tracker module records history of different events (e.g., RX and TX IPv6 messages or network info changes, etc.) as the Thread network operates. All tracked entries are timestamped.
+
+All commands under `history` require `OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE` feature to be enabled.
+
+The number of entries recorded for each history list is configurable through a set of OpenThread config options, e.g. `OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE` specifies the number of entries in Network Info history list. The History Tracker will keep the most recent entries overwriting oldest one when the list gets full.
+
+## Command List
+
+Usage : `history [command] ...`
+
+- [help](#help)
+- [netinfo](#netinfo)
+- [rx](#rx)
+- [rxtx](#rxtx)
+- [tx](#tx)
+
+## Timestamp Format
+
+Recorded entries are timestamped. When the history list is printed, the timestamps are shown relative the time the command was issues (i.e., when the list was printed) indicating how long ago the entry was recorded.
+
+```bash
+> history netinfo
+| Age                  | Role     | Mode | RLOC16 | Partition ID |
++----------------------+----------+------+--------+--------------+
+|         02:31:50.628 | leader   | rdn  | 0x2000 |    151029327 |
+|         02:31:53.262 | detached | rdn  | 0xfffe |            0 |
+|         02:31:54.663 | detached | rdn  | 0x2000 |            0 |
+Done
+```
+
+For example `02:31:50.628` indicates the event was recorded "2 hours, 31 minutes, 50 seconds, and 628 milliseconds ago". Number of days is added for events that are older than 24 hours, e.g., `1 day 11:25:31.179`, or `31 days 03:00:23.931`.
+
+Timestamps use millisecond accuracy and are tacked up to 49 days. If the event is older than 49 days, the entry is still tracked in the list but the timestamp is shown as `more than 49 days`.
+
+## Command Details
+
+### help
+
+Usage: `history help`
+
+Print SRP client help menu.
+
+```bash
+> history help
+help
+netinfo
+rx
+rxtx
+tx
+Done
+>
+```
+
+### netinfo
+
+Usage `history netinfo [list] [<num-entries>]`
+
+Print the Network Info history. Each Network Info provides
+
+- Device Role
+- MLE Link Mode
+- RLOC16
+- Partition ID
+
+Print the Network Info history as a table.
+
+```bash
+> history netinfo
+| Age                  | Role     | Mode | RLOC16 | Partition ID |
++----------------------+----------+------+--------+--------------+
+|         00:00:10.069 | router   | rdn  | 0x6000 |    151029327 |
+|         00:02:09.337 | child    | rdn  | 0x2001 |    151029327 |
+|         00:02:09.338 | child    | rdn  | 0x2001 |    151029327 |
+|         00:07:40.806 | child    | -    | 0x2001 |    151029327 |
+|         00:07:42.297 | detached | -    | 0x6000 |            0 |
+|         00:07:42.968 | disabled | -    | 0x6000 |            0 |
+Done
+```
+
+Print the Network Info history as a list.
+
+```bash
+> history netinfo list
+00:00:59.467 -> role:router mode:rdn rloc16:0x6000 partition-id:151029327
+00:02:58.735 -> role:child mode:rdn rloc16:0x2001 partition-id:151029327
+00:02:58.736 -> role:child mode:rdn rloc16:0x2001 partition-id:151029327
+00:08:30.204 -> role:child mode:- rloc16:0x2001 partition-id:151029327
+00:08:31.695 -> role:detached mode:- rloc16:0x6000 partition-id:0
+00:08:32.366 -> role:disabled mode:- rloc16:0x6000 partition-id:0
+Done
+```
+
+Print only the latest 2 entries.
+
+```bash
+> history netinfo 2
+| Age                  | Role     | Mode | RLOC16 | Partition ID |
++----------------------+----------+------+--------+--------------+
+|         00:02:05.451 | router   | rdn  | 0x6000 |    151029327 |
+|         00:04:04.719 | child    | rdn  | 0x2001 |    151029327 |
+Done
+```
+
+### rx
+
+Usage `history rx [list] [<num-entries>]`
+
+Print the IPv6 message RX history in either table or list format. Entries provide same information and follow same format as in `history rxtx` command.
+
+Print the IPv6 message RX history as a table:
+
+```bash
+> history rx
+| Age                  | Type             | Len   | Chksum | Sec | Prio | RSS  |Dir | Neighb | Radio |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    50 | 0xbd26 |  no |  net |  -20 | RX | 0x4800 |  15.4 |
+|         00:00:07.640 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788                                 |
+|                      | dst: [ff02:0:0:0:0:0:0:1]:19788                                             |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | HopOpts          |    44 | 0x0000 | yes | norm |  -20 | RX | 0x4800 |  15.4 |
+|         00:00:09.263 | src: [fdde:ad00:beef:0:0:ff:fe00:4800]:0                                    |
+|                      | dst: [ff03:0:0:0:0:0:0:2]:0                                                 |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    12 | 0x3f7d | yes |  net |  -20 | RX | 0x4800 |  15.4 |
+|         00:00:09.302 | src: [fdde:ad00:beef:0:0:ff:fe00:4800]:61631                                |
+|                      | dst: [fdde:ad00:beef:0:0:ff:fe00:4801]:61631                                |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | ICMP6(EchoReqst) |    16 | 0x942c | yes | norm |  -20 | RX | 0x4800 |  15.4 |
+|         00:00:09.304 | src: [fdde:ad00:beef:0:ac09:a16b:3204:dc09]:0                               |
+|                      | dst: [fdde:ad00:beef:0:dc0e:d6b3:f180:b75b]:0                               |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | HopOpts          |    44 | 0x0000 | yes | norm |  -20 | RX | 0x4800 |  15.4 |
+|         00:00:09.304 | src: [fdde:ad00:beef:0:0:ff:fe00:4800]:0                                    |
+|                      | dst: [ff03:0:0:0:0:0:0:2]:0                                                 |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    50 | 0x2e37 |  no |  net |  -20 | RX | 0x4800 |  15.4 |
+|         00:00:21.622 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788                                 |
+|                      | dst: [ff02:0:0:0:0:0:0:1]:19788                                             |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    50 | 0xe177 |  no |  net |  -20 | RX | 0x4800 |  15.4 |
+|         00:00:26.640 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788                                 |
+|                      | dst: [ff02:0:0:0:0:0:0:1]:19788                                             |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |   165 | 0x82ee | yes |  net |  -20 | RX | 0x4800 |  15.4 |
+|         00:00:30.000 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788                                 |
+|                      | dst: [fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788                                  |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    93 | 0x52df |  no |  net |  -20 | RX | unknwn |  15.4 |
+|         00:00:30.480 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788                                 |
+|                      | dst: [fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788                                  |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    50 | 0x5ccf |  no |  net |  -20 | RX | unknwn |  15.4 |
+|         00:00:30.772 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788                                 |
+|                      | dst: [ff02:0:0:0:0:0:0:1]:19788                                             |
+Done
+
+```
+
+Print the latest 5 entries of the IPv6 message RX history as a list:
+
+```bash
+> history rx list 4
+00:00:13.368
+    type:UDP len:50 cheksum:0xbd26 sec:no prio:net rss:-20 from:0x4800 radio:15.4
+    src:[fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788
+    dst:[ff02:0:0:0:0:0:0:1]:19788
+00:00:14.991
+    type:HopOpts len:44 cheksum:0x0000 sec:yes prio:norm rss:-20 from:0x4800 radio:15.4
+    src:[fdde:ad00:beef:0:0:ff:fe00:4800]:0
+    dst:[ff03:0:0:0:0:0:0:2]:0
+00:00:15.030
+    type:UDP len:12 cheksum:0x3f7d sec:yes prio:net rss:-20 from:0x4800 radio:15.4
+    src:[fdde:ad00:beef:0:0:ff:fe00:4800]:61631
+    dst:[fdde:ad00:beef:0:0:ff:fe00:4801]:61631
+00:00:15.032
+    type:ICMP6(EchoReqst) len:16 cheksum:0x942c sec:yes prio:norm rss:-20 from:0x4800 radio:15.4
+    src:[fdde:ad00:beef:0:ac09:a16b:3204:dc09]:0
+    dst:[fdde:ad00:beef:0:dc0e:d6b3:f180:b75b]:0
+Done
+```
+
+### rxtx
+
+Usage `history rxtx [list] [<num-entries>]`
+
+Print the combined IPv6 message RX and TX history in either table or list format. Each entry provides:
+
+- IPv6 message type: UDP, TCP, ICMP6 (and its subtype), etc.
+- IPv6 payload length (excludes the IPv6 header).
+- Source IPv6 address and port number.
+- Destination IPv6 address and port number (port number is valid for UDP/TCP, it is zero otherwise).
+- Whether or not link-layer security was used.
+- Message priority: low, norm, high, net (for Thread control messages).
+- Message checksum (valid for UDP, TCP, or ICMP6 message)
+- RSS: Received Signal Strength (in dBm) - averaged over all received fragment frames that formed the message. For TX history `NA` (not applicable) is used.
+- Whether the message was sent or received (`TX` or `RX`). A failed transmission (e.g., if tx was aborted or no ack from peer for any of the message fragments) is indicated with `TX-F` in the table format or `tx-success:no` in the list format.
+- Short address (RLOC16) of neighbor to/from which the message was sent/received. If the frame is broadcast, it is shown as `bcast` in table format or `0xffff` in the list format. If the short address of neighbor is not available, it is shown as `unknwn` in the table format or `0xfffe` in the list format.
+- Radio link on which the message was sent/received (useful when `OPENTHREAD_CONFIG_MULTI_RADIO` is enabled). Can be `15.4`, `trel`, or `all` (if sent on all radio links).
+
+Print the IPv6 message RX and TX history as a table:
+
+```bash
+> history rxtx
+| Age                  | Type             | Len   | Chksum | Sec | Prio | RSS  |Dir | Neighb | Radio |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | HopOpts          |    44 | 0x0000 | yes | norm |  -20 | RX | 0x0800 |  15.4 |
+|         00:00:09.267 | src: [fdde:ad00:beef:0:0:ff:fe00:800]:0                                     |
+|                      | dst: [ff03:0:0:0:0:0:0:2]:0                                                 |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    12 | 0x6c6b | yes |  net |  -20 | RX | 0x0800 |  15.4 |
+|         00:00:09.290 | src: [fdde:ad00:beef:0:0:ff:fe00:800]:61631                                 |
+|                      | dst: [fdde:ad00:beef:0:0:ff:fe00:801]:61631                                 |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | ICMP6(EchoReqst) |    16 | 0xc6a2 | yes | norm |  -20 | RX | 0x0800 |  15.4 |
+|         00:00:09.292 | src: [fdde:ad00:beef:0:efe8:4910:cf95:dee9]:0                               |
+|                      | dst: [fdde:ad00:beef:0:af4c:3644:882a:3698]:0                               |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | ICMP6(EchoReply) |    16 | 0xc5a2 | yes | norm |  NA  | TX | 0x0800 |  15.4 |
+|         00:00:09.292 | src: [fdde:ad00:beef:0:af4c:3644:882a:3698]:0                               |
+|                      | dst: [fdde:ad00:beef:0:efe8:4910:cf95:dee9]:0                               |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    50 | 0xaa0d | yes |  net |  NA  | TX | 0x0800 |  15.4 |
+|         00:00:09.294 | src: [fdde:ad00:beef:0:0:ff:fe00:801]:61631                                 |
+|                      | dst: [fdde:ad00:beef:0:0:ff:fe00:800]:61631                                 |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | HopOpts          |    44 | 0x0000 | yes | norm |  -20 | RX | 0x0800 |  15.4 |
+|         00:00:09.296 | src: [fdde:ad00:beef:0:0:ff:fe00:800]:0                                     |
+|                      | dst: [ff03:0:0:0:0:0:0:2]:0                                                 |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    50 | 0xc1d8 |  no |  net |  -20 | RX | 0x0800 |  15.4 |
+|         00:00:09.569 | src: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788                                 |
+|                      | dst: [ff02:0:0:0:0:0:0:1]:19788                                             |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    50 | 0x3cb1 |  no |  net |  -20 | RX | 0x0800 |  15.4 |
+|         00:00:16.519 | src: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788                                 |
+|                      | dst: [ff02:0:0:0:0:0:0:1]:19788                                             |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    50 | 0xeda0 |  no |  net |  -20 | RX | 0x0800 |  15.4 |
+|         00:00:20.599 | src: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788                                 |
+|                      | dst: [ff02:0:0:0:0:0:0:1]:19788                                             |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |   165 | 0xbdfa | yes |  net |  -20 | RX | 0x0800 |  15.4 |
+|         00:00:21.059 | src: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788                                 |
+|                      | dst: [fe80:0:0:0:8893:c2cc:d983:1e1c]:19788                                 |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    64 | 0x1c11 |  no |  net |  NA  | TX | 0x0800 |  15.4 |
+|         00:00:21.062 | src: [fe80:0:0:0:8893:c2cc:d983:1e1c]:19788                                 |
+|                      | dst: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788                                 |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    93 | 0xedff |  no |  net |  -20 | RX | unknwn |  15.4 |
+|         00:00:21.474 | src: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788                                 |
+|                      | dst: [fe80:0:0:0:8893:c2cc:d983:1e1c]:19788                                 |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    44 | 0xd383 |  no |  net |  NA  | TX | bcast  |  15.4 |
+|         00:00:21.811 | src: [fe80:0:0:0:8893:c2cc:d983:1e1c]:19788                                 |
+|                      | dst: [ff02:0:0:0:0:0:0:2]:19788                                             |
+Done
+```
+
+Print the latest 5 entries of the IPv6 message RX history as a list:
+
+```bash
+> history rxtx list 5
+
+00:00:02.100
+    type:UDP len:50 cheksum:0xd843 sec:no prio:net rss:-20 from:0x0800 radio:15.4
+    src:[fe80:0:0:0:54d9:5153:ffc6:df26]:19788
+    dst:[ff02:0:0:0:0:0:0:1]:19788
+00:00:15.331
+    type:HopOpts len:44 cheksum:0x0000 sec:yes prio:norm rss:-20 from:0x0800 radio:15.4
+    src:[fdde:ad00:beef:0:0:ff:fe00:800]:0
+    dst:[ff03:0:0:0:0:0:0:2]:0
+00:00:15.354
+    type:UDP len:12 cheksum:0x6c6b sec:yes prio:net rss:-20 from:0x0800 radio:15.4
+    src:[fdde:ad00:beef:0:0:ff:fe00:800]:61631
+    dst:[fdde:ad00:beef:0:0:ff:fe00:801]:61631
+00:00:15.356
+    type:ICMP6(EchoReqst) len:16 cheksum:0xc6a2 sec:yes prio:norm rss:-20 from:0x0800 radio:15.4
+    src:[fdde:ad00:beef:0:efe8:4910:cf95:dee9]:0
+    dst:[fdde:ad00:beef:0:af4c:3644:882a:3698]:0
+00:00:15.356
+    type:ICMP6(EchoReply) len:16 cheksum:0xc5a2 sec:yes prio:norm tx-success:yes to:0x0800 radio:15.4
+    src:[fdde:ad00:beef:0:af4c:3644:882a:3698]:0
+    dst:[fdde:ad00:beef:0:efe8:4910:cf95:dee9]:0
+```
+
+### tx
+
+Usage `history tx [list] [<num-entries>]`
+
+Print the IPv6 message TX history in either table or list format. Entries provide same information and follow same format as in `history rxtx` command.
+
+Print the IPv6 message TX history as a table (10 latest entries):
+
+```bash
+> history tx
+| Age                  | Type             | Len   | Chksum | Sec | Prio | RSS  |Dir | Neighb | Radio |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | ICMP6(EchoReply) |    16 | 0x932c | yes | norm |  NA  | TX | 0x4800 |  15.4 |
+|         00:00:18.798 | src: [fdde:ad00:beef:0:dc0e:d6b3:f180:b75b]:0                               |
+|                      | dst: [fdde:ad00:beef:0:ac09:a16b:3204:dc09]:0                               |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    50 | 0xce87 | yes |  net |  NA  | TX | 0x4800 |  15.4 |
+|         00:00:18.800 | src: [fdde:ad00:beef:0:0:ff:fe00:4801]:61631                                |
+|                      | dst: [fdde:ad00:beef:0:0:ff:fe00:4800]:61631                                |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    64 | 0xf7ba |  no |  net |  NA  | TX | 0x4800 |  15.4 |
+|         00:00:39.499 | src: [fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788                                  |
+|                      | dst: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788                                 |
++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+|                      | UDP              |    44 | 0x26d4 |  no |  net |  NA  | TX | bcast  |  15.4 |
+|         00:00:40.256 | src: [fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788                                  |
+|                      | dst: [ff02:0:0:0:0:0:0:2]:19788                                             |
+Done
+```
+
+Print the IPv6 message TX history as a list:
+
+```bash
+history tx list
+00:00:23.957
+    type:ICMP6(EchoReply) len:16 cheksum:0x932c sec:yes prio:norm tx-success:yes to:0x4800 radio:15.4
+    src:[fdde:ad00:beef:0:dc0e:d6b3:f180:b75b]:0
+    dst:[fdde:ad00:beef:0:ac09:a16b:3204:dc09]:0
+00:00:23.959
+    type:UDP len:50 cheksum:0xce87 sec:yes prio:net tx-success:yes to:0x4800 radio:15.4
+    src:[fdde:ad00:beef:0:0:ff:fe00:4801]:61631
+    dst:[fdde:ad00:beef:0:0:ff:fe00:4800]:61631
+00:00:44.658
+    type:UDP len:64 cheksum:0xf7ba sec:no prio:net tx-success:yes to:0x4800 radio:15.4
+    src:[fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788
+    dst:[fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788
+00:00:45.415
+    type:UDP len:44 cheksum:0x26d4 sec:no prio:net tx-success:yes to:0xffff radio:15.4
+    src:[fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788
+    dst:[ff02:0:0:0:0:0:0:2]:19788
+Done
+```
diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp
index 7f6e97a..9c8e549 100644
--- a/src/cli/cli.cpp
+++ b/src/cli/cli.cpp
@@ -128,6 +128,9 @@
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
     , mSrpServer(*this)
 #endif
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+    , mHistory(*this)
+#endif
 #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE
     , mOutputLength(0)
     , mIsLogging(false)
@@ -162,6 +165,35 @@
     }
 }
 
+const char *Interpreter::LinkModeToString(const otLinkModeConfig &aLinkMode, char (&aStringBuffer)[kLinkModeStringSize])
+{
+    char *flagsPtr = &aStringBuffer[0];
+
+    if (aLinkMode.mRxOnWhenIdle)
+    {
+        *flagsPtr++ = 'r';
+    }
+
+    if (aLinkMode.mDeviceType)
+    {
+        *flagsPtr++ = 'd';
+    }
+
+    if (aLinkMode.mNetworkData)
+    {
+        *flagsPtr++ = 'n';
+    }
+
+    if (flagsPtr == &aStringBuffer[0])
+    {
+        *flagsPtr++ = '-';
+    }
+
+    *flagsPtr = '\0';
+
+    return aStringBuffer;
+}
+
 void Interpreter::OutputEnabledDisabledStatus(bool aEnabled)
 {
     OutputLine(aEnabled ? "Enabled" : "Disabled");
@@ -202,7 +234,11 @@
     }
 
     OutputLine("|");
+    OutputTableSeperator(aNumColumns, aWidths);
+}
 
+void Interpreter::OutputTableSeperator(uint8_t aNumColumns, const uint8_t aWidths[])
+{
     for (uint8_t index = 0; index < aNumColumns; index++)
     {
         OutputFormat("+");
@@ -324,6 +360,13 @@
     return OT_ERROR_NONE;
 }
 
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+otError Interpreter::ProcessHistory(Arg aArgs[])
+{
+    return mHistory.Process(aArgs);
+}
+#endif
+
 #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
 otError Interpreter::ProcessBorderAgent(Arg aArgs[])
 {
@@ -866,10 +909,12 @@
 #if OPENTHREAD_FTD
 otError Interpreter::ProcessChild(Arg aArgs[])
 {
-    otError     error = OT_ERROR_NONE;
-    otChildInfo childInfo;
-    uint16_t    childId;
-    bool        isTable;
+    otError          error = OT_ERROR_NONE;
+    otChildInfo      childInfo;
+    uint16_t         childId;
+    bool             isTable;
+    otLinkModeConfig linkMode;
+    char             linkModeString[kLinkModeStringSize];
 
     isTable = (aArgs[0] == "table");
 
@@ -936,32 +981,10 @@
     OutputFormat("Ext Addr: ");
     OutputExtAddress(childInfo.mExtAddress);
     OutputLine("");
-    OutputFormat("Mode: ");
-
-    if (!(childInfo.mRxOnWhenIdle || childInfo.mFullThreadDevice || childInfo.mFullNetworkData))
-    {
-        OutputFormat("-");
-    }
-    else
-    {
-        if (childInfo.mRxOnWhenIdle)
-        {
-            OutputFormat("r");
-        }
-
-        if (childInfo.mFullThreadDevice)
-        {
-            OutputFormat("d");
-        }
-
-        if (childInfo.mFullNetworkData)
-        {
-            OutputFormat("n");
-        }
-    }
-
-    OutputLine("");
-
+    linkMode.mRxOnWhenIdle = childInfo.mRxOnWhenIdle;
+    linkMode.mDeviceType   = childInfo.mFullThreadDevice;
+    linkMode.mNetworkData  = childInfo.mFullThreadDevice;
+    OutputLine("Mode: %s", LinkModeToString(linkMode, linkModeString));
     OutputLine("Net Data: %d", childInfo.mNetworkDataVersion);
     OutputLine("Timeout: %d", childInfo.mTimeout);
     OutputLine("Age: %d", childInfo.mAge);
@@ -2663,32 +2686,9 @@
 
     if (aArgs[0].IsEmpty())
     {
-        linkMode = otThreadGetLinkMode(mInstance);
+        char linkModeString[kLinkModeStringSize];
 
-        if (!(linkMode.mRxOnWhenIdle || linkMode.mDeviceType || linkMode.mNetworkData))
-        {
-            OutputFormat("-");
-        }
-        else
-        {
-            if (linkMode.mRxOnWhenIdle)
-            {
-                OutputFormat("r");
-            }
-
-            if (linkMode.mDeviceType)
-            {
-                OutputFormat("d");
-            }
-
-            if (linkMode.mNetworkData)
-            {
-                OutputFormat("n");
-            }
-        }
-
-        OutputLine("");
-
+        OutputLine("%s", LinkModeToString(otThreadGetLinkMode(mInstance), linkModeString));
         ExitNow();
     }
 
@@ -4026,34 +4026,7 @@
 
     if (aArgs[0].IsEmpty())
     {
-        switch (otThreadGetDeviceRole(mInstance))
-        {
-        case OT_DEVICE_ROLE_DISABLED:
-            OutputLine("disabled");
-            break;
-
-        case OT_DEVICE_ROLE_DETACHED:
-            OutputLine("detached");
-            break;
-
-        case OT_DEVICE_ROLE_CHILD:
-            OutputLine("child");
-            break;
-
-#if OPENTHREAD_FTD
-        case OT_DEVICE_ROLE_ROUTER:
-            OutputLine("router");
-            break;
-
-        case OT_DEVICE_ROLE_LEADER:
-            OutputLine("leader");
-            break;
-#endif
-
-        default:
-            OutputLine("invalid state");
-            break;
-        }
+        OutputLine("%s", otThreadDeviceRoleToString(otThreadGetDeviceRole(mInstance)));
     }
     else
     {
diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp
index 2eafb7f..f2ca60e 100644
--- a/src/cli/cli.hpp
+++ b/src/cli/cli.hpp
@@ -56,6 +56,7 @@
 
 #include "cli/cli_commissioner.hpp"
 #include "cli/cli_dataset.hpp"
+#include "cli/cli_history.hpp"
 #include "cli/cli_joiner.hpp"
 #include "cli/cli_network_data.hpp"
 #include "cli/cli_srp_client.hpp"
@@ -100,6 +101,7 @@
     friend class CoapSecure;
     friend class Commissioner;
     friend class Dataset;
+    friend class History;
     friend class Joiner;
     friend class NetworkData;
     friend class SrpClient;
@@ -306,6 +308,22 @@
      */
     void SetUserCommands(const otCliCommand *aCommands, uint8_t aLength, void *aContext);
 
+    static constexpr uint8_t kLinkModeStringSize = sizeof("rdn"); ///< Size of string buffer for a MLE Link Mode.
+
+    /**
+     * This method converts a given MLE Link Mode to flag string.
+     *
+     * The characters 'r', 'd', and 'n' are respectively used for `mRxOnWhenIdle`, `mDeviceType` and `mNetworkData`
+     * flags. If all flags are `false`, then "-" is returned.
+     *
+     * @param[in]  aLinkMode       The MLE Link Mode to convert.
+     * @param[out] aStringBuffer   A reference to an string array to place the string.
+     *
+     * @returns A pointer @p aStringBuffer which contains the converted string.
+     *
+     */
+    static const char *LinkModeToString(const otLinkModeConfig &aLinkMode, char (&aStringBuffer)[kLinkModeStringSize]);
+
 protected:
     static Interpreter *sInterpreter;
 
@@ -401,6 +419,7 @@
     }
 
     void OutputTableHeader(uint8_t aNumColumns, const char *const aTitles[], const uint8_t aWidths[]);
+    void OutputTableSeperator(uint8_t aNumColumns, const uint8_t aWidths[]);
 
     template <uint8_t kTableNumColumns>
     void OutputTableHeader(const char *const (&aTitles)[kTableNumColumns], const uint8_t (&aWidths)[kTableNumColumns])
@@ -408,6 +427,11 @@
         OutputTableHeader(kTableNumColumns, &aTitles[0], aWidths);
     }
 
+    template <uint8_t kTableNumColumns> void OutputTableSeperator(const uint8_t (&aWidths)[kTableNumColumns])
+    {
+        OutputTableSeperator(kTableNumColumns, aWidths);
+    }
+
 #if OPENTHREAD_CONFIG_PING_SENDER_ENABLE
     otError ParsePingInterval(const Arg &aArg, uint32_t &aInterval);
 #endif
@@ -419,6 +443,7 @@
 
     otError ProcessUserCommands(Arg aArgs[]);
     otError ProcessHelp(Arg aArgs[]);
+    otError ProcessHistory(Arg aArgs[]);
     otError ProcessCcaThreshold(Arg aArgs[]);
     otError ProcessBufferInfo(Arg aArgs[]);
     otError ProcessChannel(Arg aArgs[]);
@@ -804,6 +829,9 @@
 #endif
         {"fem", &Interpreter::ProcessFem},
         {"help", &Interpreter::ProcessHelp},
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+        {"history", &Interpreter::ProcessHistory},
+#endif
         {"ifconfig", &Interpreter::ProcessIfconfig},
         {"ipaddr", &Interpreter::ProcessIpAddr},
         {"ipmaddr", &Interpreter::ProcessIpMulticastAddr},
@@ -953,6 +981,10 @@
     SrpServer mSrpServer;
 #endif
 
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+    History mHistory;
+#endif
+
 #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE
     char     mOutputString[OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LOG_STRING_SIZE];
     uint16_t mOutputLength;
diff --git a/src/cli/cli_history.cpp b/src/cli/cli_history.cpp
new file mode 100644
index 0000000..d5ede55
--- /dev/null
+++ b/src/cli/cli_history.cpp
@@ -0,0 +1,423 @@
+/*
+ *  Copyright (c) 2021, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file implements CLI for the History Tracker.
+ */
+
+#include "cli_history.hpp"
+
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+
+#include <string.h>
+
+#include "cli/cli.hpp"
+
+namespace ot {
+namespace Cli {
+
+constexpr History::Command History::sCommands[];
+
+otError History::ProcessHelp(Arg aArgs[])
+{
+    OT_UNUSED_VARIABLE(aArgs);
+
+    for (const Command &command : sCommands)
+    {
+        mInterpreter.OutputLine(command.mName);
+    }
+
+    return OT_ERROR_NONE;
+}
+
+otError History::ParseArgs(Arg aArgs[], bool &aIsList, uint16_t &aNumEntries) const
+{
+    if (*aArgs == "list")
+    {
+        aArgs++;
+        aIsList = true;
+    }
+    else
+    {
+        aIsList = false;
+    }
+
+    if (aArgs->ParseAsUint16(aNumEntries) == OT_ERROR_NONE)
+    {
+        aArgs++;
+    }
+    else
+    {
+        aNumEntries = 0;
+    }
+
+    return aArgs[0].IsEmpty() ? OT_ERROR_NONE : OT_ERROR_INVALID_ARGS;
+}
+
+otError History::ProcessNetInfo(Arg aArgs[])
+{
+    otError                            error;
+    bool                               isList;
+    uint16_t                           numEntries;
+    otHistoryTrackerIterator           iterator;
+    const otHistoryTrackerNetworkInfo *info;
+    uint32_t                           entryAge;
+    char                               ageString[OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE];
+    char                               linkModeString[Interpreter::kLinkModeStringSize];
+
+    SuccessOrExit(error = ParseArgs(aArgs, isList, numEntries));
+
+    if (!isList)
+    {
+        // | Age                  | Role     | Mode | RLOC16 | Partition ID |
+        // +----------------------+----------+------+--------+--------------+
+
+        static const char *const kNetInfoTitles[]       = {"Age", "Role", "Mode", "RLOC16", "Partition ID"};
+        static const uint8_t     kNetInfoColumnWidths[] = {22, 10, 6, 8, 14};
+
+        mInterpreter.OutputTableHeader(kNetInfoTitles, kNetInfoColumnWidths);
+    }
+
+    otHistoryTrackerInitIterator(&iterator);
+
+    for (uint16_t index = 0; (numEntries == 0) || (index < numEntries); index++)
+    {
+        info = otHistoryTrackerIterateNetInfoHistory(mInterpreter.mInstance, &iterator, &entryAge);
+        VerifyOrExit(info != nullptr);
+
+        otHistoryTrackerEntryAgeToString(entryAge, ageString, sizeof(ageString));
+
+        mInterpreter.OutputLine(
+            isList ? "%s -> role:%s mode:%s rloc16:0x%04x partition-id:%u" : "| %20s | %-8s | %-4s | 0x%04x | %12u |",
+            ageString, otThreadDeviceRoleToString(info->mRole),
+            Interpreter::LinkModeToString(info->mMode, linkModeString), info->mRloc16, info->mPartitionId);
+    }
+
+exit:
+    return error;
+}
+
+otError History::ProcessRx(Arg aArgs[])
+{
+    return ProcessRxTxHistory(kRx, aArgs);
+}
+
+otError History::ProcessRxTx(Arg aArgs[])
+{
+    return ProcessRxTxHistory(kRxTx, aArgs);
+}
+
+otError History::ProcessTx(Arg aArgs[])
+{
+    return ProcessRxTxHistory(kTx, aArgs);
+}
+
+const char *History::MessagePriorityToString(uint8_t aPriority)
+{
+    const char *str = "unkn";
+
+    switch (aPriority)
+    {
+    case OT_HISTORY_TRACKER_MSG_PRIORITY_LOW:
+        str = "low";
+        break;
+
+    case OT_HISTORY_TRACKER_MSG_PRIORITY_NORMAL:
+        str = "norm";
+        break;
+
+    case OT_HISTORY_TRACKER_MSG_PRIORITY_HIGH:
+        str = "high";
+        break;
+
+    case OT_HISTORY_TRACKER_MSG_PRIORITY_NET:
+        str = "net";
+        break;
+
+    default:
+        break;
+    }
+
+    return str;
+}
+
+const char *History::RadioTypeToString(const otHistoryTrackerMessageInfo &aInfo)
+{
+    const char *str = "none";
+
+    if (aInfo.mRadioTrelUdp6 && aInfo.mRadioIeee802154)
+    {
+        str = "all";
+    }
+    else if (aInfo.mRadioIeee802154)
+    {
+        str = "15.4";
+    }
+    else if (aInfo.mRadioTrelUdp6)
+    {
+        str = "trel";
+    }
+
+    return str;
+}
+
+const char *History::MessageTypeToString(const otHistoryTrackerMessageInfo &aInfo)
+{
+    const char *str = otIp6ProtoToString(aInfo.mIpProto);
+
+    if (aInfo.mIpProto == OT_IP6_PROTO_ICMP6)
+    {
+        switch (aInfo.mIcmp6Type)
+        {
+        case OT_ICMP6_TYPE_DST_UNREACH:
+            str = "ICMP6(Unreach)";
+            break;
+        case OT_ICMP6_TYPE_PACKET_TO_BIG:
+            str = "ICMP6(TooBig)";
+            break;
+        case OT_ICMP6_TYPE_ECHO_REQUEST:
+            str = "ICMP6(EchoReqst)";
+            break;
+        case OT_ICMP6_TYPE_ECHO_REPLY:
+            str = "ICMP6(EchoReply)";
+            break;
+        case OT_ICMP6_TYPE_ROUTER_SOLICIT:
+            str = "ICMP6(RouterSol)";
+            break;
+        case OT_ICMP6_TYPE_ROUTER_ADVERT:
+            str = "ICMP6(RouterAdv)";
+            break;
+        default:
+            str = "ICMP6(Other)";
+            break;
+        }
+    }
+
+    return str;
+}
+
+otError History::ProcessRxTxHistory(RxTx aRxTx, Arg aArgs[])
+{
+    otError                            error;
+    bool                               isList;
+    uint16_t                           numEntries;
+    otHistoryTrackerIterator           rxIterator;
+    otHistoryTrackerIterator           txIterator;
+    bool                               isRx   = false;
+    const otHistoryTrackerMessageInfo *info   = nullptr;
+    const otHistoryTrackerMessageInfo *rxInfo = nullptr;
+    const otHistoryTrackerMessageInfo *txInfo = nullptr;
+    uint32_t                           entryAge;
+    uint32_t                           rxEntryAge;
+    uint32_t                           txEntryAge;
+
+    // | Age                  | Type             | Len   | Chksum | Sec | Prio | RSS  |Dir | Neighb | Radio |
+    // +----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+
+
+    static const char *const kTableTitles[] = {"Age",  "Type", "Len", "Chksum", "Sec",
+                                               "Prio", "RSS",  "Dir", "Neighb", "Radio"};
+
+    static const uint8_t kTableColumnWidths[] = {22, 18, 7, 8, 5, 6, 6, 4, 8, 7};
+
+    SuccessOrExit(error = ParseArgs(aArgs, isList, numEntries));
+
+    if (!isList)
+    {
+        mInterpreter.OutputTableHeader(kTableTitles, kTableColumnWidths);
+    }
+
+    otHistoryTrackerInitIterator(&txIterator);
+    otHistoryTrackerInitIterator(&rxIterator);
+
+    for (uint16_t index = 0; (numEntries == 0) || (index < numEntries); index++)
+    {
+        switch (aRxTx)
+        {
+        case kRx:
+            info = otHistoryTrackerIterateRxHistory(mInterpreter.mInstance, &rxIterator, &entryAge);
+            isRx = true;
+            break;
+
+        case kTx:
+            info = otHistoryTrackerIterateTxHistory(mInterpreter.mInstance, &txIterator, &entryAge);
+            isRx = false;
+            break;
+
+        case kRxTx:
+            // Iterate through both RX and TX lists and determine the entry
+            // with earlier age.
+
+            if (rxInfo == nullptr)
+            {
+                rxInfo = otHistoryTrackerIterateRxHistory(mInterpreter.mInstance, &rxIterator, &rxEntryAge);
+            }
+
+            if (txInfo == nullptr)
+            {
+                txInfo = otHistoryTrackerIterateTxHistory(mInterpreter.mInstance, &txIterator, &txEntryAge);
+            }
+
+            if ((rxInfo != nullptr) && ((txInfo == nullptr) || (rxEntryAge <= txEntryAge)))
+            {
+                info     = rxInfo;
+                entryAge = rxEntryAge;
+                isRx     = true;
+                rxInfo   = nullptr;
+            }
+            else
+            {
+                info     = txInfo;
+                entryAge = txEntryAge;
+                isRx     = false;
+                txInfo   = nullptr;
+            }
+
+            break;
+        }
+
+        VerifyOrExit(info != nullptr);
+
+        if (isList)
+        {
+            OutputRxTxEntryListFormat(*info, entryAge, isRx);
+        }
+        else
+        {
+            if (index != 0)
+            {
+                mInterpreter.OutputTableSeperator(kTableColumnWidths);
+            }
+
+            OutputRxTxEntryTableFormat(*info, entryAge, isRx);
+        }
+    }
+
+exit:
+    return error;
+}
+
+void History::OutputRxTxEntryListFormat(const otHistoryTrackerMessageInfo &aInfo, uint32_t aEntryAge, bool aIsRx)
+{
+    constexpr uint8_t kIndentSize = 4;
+
+    char ageString[OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE];
+    char addrString[OT_IP6_SOCK_ADDR_STRING_SIZE];
+
+    otHistoryTrackerEntryAgeToString(aEntryAge, ageString, sizeof(ageString));
+
+    mInterpreter.OutputLine("%s", ageString);
+    mInterpreter.OutputFormat(kIndentSize, "type:%s len:%u cheksum:0x%04x sec:%s prio:%s ", MessageTypeToString(aInfo),
+                              aInfo.mPayloadLength, aInfo.mChecksum, aInfo.mLinkSecurity ? "yes" : "no",
+                              MessagePriorityToString(aInfo.mPriority));
+    if (aIsRx)
+    {
+        mInterpreter.OutputFormat("rss:%d", aInfo.mAveRxRss);
+    }
+    else
+    {
+        mInterpreter.OutputFormat("tx-success:%s", aInfo.mTxSuccess ? "yes" : "no");
+    }
+
+    mInterpreter.OutputLine(" %s:0x%04x radio:%s", aIsRx ? "from" : "to", aInfo.mNeighborRloc16,
+                            RadioTypeToString(aInfo));
+
+    otIp6SockAddrToString(&aInfo.mSource, addrString, sizeof(addrString));
+    mInterpreter.OutputLine(kIndentSize, "src:%s", addrString);
+
+    otIp6SockAddrToString(&aInfo.mDestination, addrString, sizeof(addrString));
+    mInterpreter.OutputLine(kIndentSize, "dst:%s", addrString);
+}
+
+void History::OutputRxTxEntryTableFormat(const otHistoryTrackerMessageInfo &aInfo, uint32_t aEntryAge, bool aIsRx)
+{
+    char ageString[OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE];
+    char addrString[OT_IP6_SOCK_ADDR_STRING_SIZE];
+
+    otHistoryTrackerEntryAgeToString(aEntryAge, ageString, sizeof(ageString));
+
+    mInterpreter.OutputFormat("| %20s | %-16.16s | %5u | 0x%04x | %3s | %4s | ", "", MessageTypeToString(aInfo),
+                              aInfo.mPayloadLength, aInfo.mChecksum, aInfo.mLinkSecurity ? "yes" : "no",
+                              MessagePriorityToString(aInfo.mPriority));
+
+    if (aIsRx)
+    {
+        mInterpreter.OutputFormat("%4d | RX ", aInfo.mAveRxRss);
+    }
+    else
+    {
+        mInterpreter.OutputFormat(" NA  |");
+        mInterpreter.OutputFormat(aInfo.mTxSuccess ? " TX " : "TX-F");
+    }
+
+    if (aInfo.mNeighborRloc16 == kShortAddrBroadcast)
+    {
+        mInterpreter.OutputFormat("| bcast  ");
+    }
+    else if (aInfo.mNeighborRloc16 == kShortAddrInvalid)
+    {
+        mInterpreter.OutputFormat("| unknwn ");
+    }
+    else
+    {
+        mInterpreter.OutputFormat("| 0x%04x ", aInfo.mNeighborRloc16);
+    }
+
+    mInterpreter.OutputLine("| %5.5s |", RadioTypeToString(aInfo));
+
+    otIp6SockAddrToString(&aInfo.mSource, addrString, sizeof(addrString));
+    mInterpreter.OutputLine("| %20s | src: %-70s |", ageString, addrString);
+
+    otIp6SockAddrToString(&aInfo.mDestination, addrString, sizeof(addrString));
+    mInterpreter.OutputLine("| %20s | dst: %-70s |", "", addrString);
+}
+
+otError History::Process(Arg aArgs[])
+{
+    otError        error = OT_ERROR_INVALID_COMMAND;
+    const Command *command;
+
+    if (aArgs[0].IsEmpty())
+    {
+        IgnoreError(ProcessHelp(aArgs));
+        ExitNow();
+    }
+
+    command = Utils::LookupTable::Find(aArgs[0].GetCString(), sCommands);
+    VerifyOrExit(command != nullptr);
+
+    error = (this->*command->mHandler)(aArgs + 1);
+
+exit:
+    return error;
+}
+
+} // namespace Cli
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
diff --git a/src/cli/cli_history.hpp b/src/cli/cli_history.hpp
new file mode 100644
index 0000000..223a705
--- /dev/null
+++ b/src/cli/cli_history.hpp
@@ -0,0 +1,128 @@
+/*
+ *  Copyright (c) 2021, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file contains definitions for CLI to control History Tracker
+ */
+
+#ifndef CLI_HISTORY_HPP_
+#define CLI_HISTORY_HPP_
+
+#include "openthread-core-config.h"
+
+#include <openthread/history_tracker.h>
+
+#include "cli/cli_config.h"
+#include "utils/lookup_table.hpp"
+#include "utils/parse_cmdline.hpp"
+
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+
+namespace ot {
+namespace Cli {
+
+class Interpreter;
+
+/**
+ * This class implements the History Tracker CLI interpreter.
+ *
+ */
+class History
+{
+public:
+    typedef Utils::CmdLineParser::Arg Arg;
+
+    /**
+     * Constructor
+     *
+     * @param[in]  aInterpreter  The CLI interpreter.
+     *
+     */
+    explicit History(Interpreter &aInterpreter)
+        : mInterpreter(aInterpreter)
+    {
+    }
+
+    /**
+     * This method interprets a list of CLI arguments.
+     *
+     * @param[in]  aArgs        A pointer an array of command line arguments.
+     *
+     */
+    otError Process(Arg aArgs[]);
+
+private:
+    static constexpr uint16_t kShortAddrInvalid   = 0xfffe;
+    static constexpr uint16_t kShortAddrBroadcast = 0xffff;
+    static constexpr int8_t   kInvalidRss         = OT_RADIO_RSSI_INVALID;
+
+    struct Command
+    {
+        const char *mName;
+        otError (History::*mHandler)(Arg aArgs[]);
+    };
+
+    enum RxTx : uint8_t
+    {
+        kRx,
+        kTx,
+        kRxTx,
+    };
+
+    otError ProcessHelp(Arg aArgs[]);
+    otError ProcessNetInfo(Arg aArgs[]);
+    otError ProcessRx(Arg aArgs[]);
+    otError ProcessRxTx(Arg aArgs[]);
+    otError ProcessTx(Arg aArgs[]);
+
+    otError ParseArgs(Arg aArgs[], bool &aIsList, uint16_t &aNumEntries) const;
+    otError ProcessRxTxHistory(RxTx aRxTx, Arg aArgs[]);
+    void    OutputRxTxEntryListFormat(const otHistoryTrackerMessageInfo &aInfo, uint32_t aEntryAge, bool aIsRx);
+    void    OutputRxTxEntryTableFormat(const otHistoryTrackerMessageInfo &aInfo, uint32_t aEntryAge, bool aIsRx);
+
+    static const char *MessagePriorityToString(uint8_t aPriority);
+    static const char *RadioTypeToString(const otHistoryTrackerMessageInfo &aInfo);
+    static const char *MessageTypeToString(const otHistoryTrackerMessageInfo &aInfo);
+
+    static constexpr Command sCommands[] = {
+        {"help", &History::ProcessHelp}, {"netinfo", &History::ProcessNetInfo}, {"rx", &History::ProcessRx},
+        {"rxtx", &History::ProcessRxTx}, {"tx", &History::ProcessTx},
+    };
+
+    static_assert(Utils::LookupTable::IsSorted(sCommands), "Command Table is not sorted");
+
+    Interpreter &mInterpreter;
+};
+
+} // namespace Cli
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+
+#endif // CLI_HISTORY_HPP_
diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn
index e860d86..30456d6 100644
--- a/src/core/BUILD.gn
+++ b/src/core/BUILD.gn
@@ -315,6 +315,7 @@
   "api/entropy_api.cpp",
   "api/error_api.cpp",
   "api/heap_api.cpp",
+  "api/history_tracker_api.cpp",
   "api/icmp6_api.cpp",
   "api/instance_api.cpp",
   "api/ip6_api.cpp",
@@ -638,6 +639,8 @@
   "utils/flash.hpp",
   "utils/heap.cpp",
   "utils/heap.hpp",
+  "utils/history_tracker.cpp",
+  "utils/history_tracker.hpp",
   "utils/jam_detector.cpp",
   "utils/jam_detector.hpp",
   "utils/lookup_table.cpp",
@@ -710,6 +713,7 @@
     "config/dns_client.h",
     "config/dnssd_server.h",
     "config/dtls.h",
+    "config/history_tracker.h",
     "config/ip6.h",
     "config/joiner.h",
     "config/link_quality.h",
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 5655f90..b6929fd 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -51,6 +51,7 @@
     api/entropy_api.cpp
     api/error_api.cpp
     api/heap_api.cpp
+    api/history_tracker_api.cpp
     api/icmp6_api.cpp
     api/instance_api.cpp
     api/ip6_api.cpp
@@ -215,6 +216,7 @@
     utils/child_supervision.cpp
     utils/flash.cpp
     utils/heap.cpp
+    utils/history_tracker.cpp
     utils/jam_detector.cpp
     utils/lookup_table.cpp
     utils/otns.cpp
diff --git a/src/core/Makefile.am b/src/core/Makefile.am
index 2d7a3ea..0f0d088 100644
--- a/src/core/Makefile.am
+++ b/src/core/Makefile.am
@@ -128,6 +128,7 @@
     api/entropy_api.cpp                           \
     api/error_api.cpp                             \
     api/heap_api.cpp                              \
+    api/history_tracker_api.cpp                   \
     api/icmp6_api.cpp                             \
     api/instance_api.cpp                          \
     api/ip6_api.cpp                               \
@@ -292,6 +293,7 @@
     utils/child_supervision.cpp                   \
     utils/flash.cpp                               \
     utils/heap.cpp                                \
+    utils/history_tracker.cpp                     \
     utils/jam_detector.cpp                        \
     utils/lookup_table.cpp                        \
     utils/otns.cpp                                \
@@ -430,6 +432,7 @@
     config/dns_client.h                           \
     config/dnssd_server.h                         \
     config/dtls.h                                 \
+    config/history_tracker.h                      \
     config/ip6.h                                  \
     config/joiner.h                               \
     config/link_quality.h                         \
@@ -562,6 +565,7 @@
     utils/child_supervision.hpp                   \
     utils/flash.hpp                               \
     utils/heap.hpp                                \
+    utils/history_tracker.hpp                     \
     utils/jam_detector.hpp                        \
     utils/lookup_table.hpp                        \
     utils/otns.hpp                                \
diff --git a/src/core/api/history_tracker_api.cpp b/src/core/api/history_tracker_api.cpp
new file mode 100644
index 0000000..e7a65d3
--- /dev/null
+++ b/src/core/api/history_tracker_api.cpp
@@ -0,0 +1,86 @@
+/*
+ *  Copyright (c) 2021, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file implements the History Tracker public APIs.
+ */
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+
+#include <openthread/history_tracker.h>
+
+#include "common/instance.hpp"
+#include "common/locator_getters.hpp"
+#include "utils/history_tracker.hpp"
+
+using namespace ot;
+
+void otHistoryTrackerInitIterator(otHistoryTrackerIterator *aIterator)
+{
+    static_cast<Utils::HistoryTracker::Iterator *>(aIterator)->Init();
+}
+
+const otHistoryTrackerNetworkInfo *otHistoryTrackerIterateNetInfoHistory(otInstance *              aInstance,
+                                                                         otHistoryTrackerIterator *aIterator,
+                                                                         uint32_t *                aEntryAge)
+{
+    Instance &instance = *static_cast<Instance *>(aInstance);
+
+    return instance.Get<Utils::HistoryTracker>().IterateNetInfoHistory(
+        *static_cast<Utils::HistoryTracker::Iterator *>(aIterator), *aEntryAge);
+}
+
+const otHistoryTrackerMessageInfo *otHistoryTrackerIterateRxHistory(otInstance *              aInstance,
+                                                                    otHistoryTrackerIterator *aIterator,
+                                                                    uint32_t *                aEntryAge)
+{
+    Instance &instance = *static_cast<Instance *>(aInstance);
+
+    return instance.Get<Utils::HistoryTracker>().IterateRxHistory(
+        *static_cast<Utils::HistoryTracker::Iterator *>(aIterator), *aEntryAge);
+}
+
+const otHistoryTrackerMessageInfo *otHistoryTrackerIterateTxHistory(otInstance *              aInstance,
+                                                                    otHistoryTrackerIterator *aIterator,
+                                                                    uint32_t *                aEntryAge)
+{
+    Instance &instance = *static_cast<Instance *>(aInstance);
+
+    return instance.Get<Utils::HistoryTracker>().IterateTxHistory(
+        *static_cast<Utils::HistoryTracker::Iterator *>(aIterator), *aEntryAge);
+}
+
+void otHistoryTrackerEntryAgeToString(uint32_t aEntryAge, char *aBuffer, uint16_t aSize)
+{
+    Utils::HistoryTracker::EntryAgeToString(aEntryAge, aBuffer, aSize);
+}
+
+#endif // OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
diff --git a/src/core/api/ip6_api.cpp b/src/core/api/ip6_api.cpp
index 5263dc3..59096a9 100644
--- a/src/core/api/ip6_api.cpp
+++ b/src/core/api/ip6_api.cpp
@@ -333,3 +333,8 @@
 }
 
 #endif
+
+const char *otIp6ProtoToString(uint8_t aIpProto)
+{
+    return Ip6::Ip6::IpProtoToString(aIpProto);
+}
diff --git a/src/core/api/thread_api.cpp b/src/core/api/thread_api.cpp
index 1ea74fc..0a90184 100644
--- a/src/core/api/thread_api.cpp
+++ b/src/core/api/thread_api.cpp
@@ -336,6 +336,11 @@
     return static_cast<otDeviceRole>(instance.Get<Mle::MleRouter>().GetRole());
 }
 
+const char *otThreadDeviceRoleToString(otDeviceRole aRole)
+{
+    return Mle::Mle::RoleToString(static_cast<Mle::DeviceRole>(aRole));
+}
+
 otError otThreadGetLeaderData(otInstance *aInstance, otLeaderData *aLeaderData)
 {
     Instance &instance = *static_cast<Instance *>(aInstance);
diff --git a/src/core/common/instance.cpp b/src/core/common/instance.cpp
index 213749c..0d3842c 100644
--- a/src/core/common/instance.cpp
+++ b/src/core/common/instance.cpp
@@ -87,6 +87,9 @@
 #if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
     , mChannelManager(*this)
 #endif
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+    , mHistoryTracker(*this)
+#endif
 #if (OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE || OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE) && OPENTHREAD_FTD
     , mDatasetUpdater(*this)
 #endif
diff --git a/src/core/common/instance.hpp b/src/core/common/instance.hpp
index e97d5af..8d056c6 100644
--- a/src/core/common/instance.hpp
+++ b/src/core/common/instance.hpp
@@ -82,6 +82,9 @@
 #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
 #include "utils/channel_monitor.hpp"
 #endif
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+#include "utils/history_tracker.hpp"
+#endif
 
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
 #include "backbone_router/bbr_leader.hpp"
@@ -388,6 +391,10 @@
     Utils::ChannelManager mChannelManager;
 #endif
 
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+    Utils::HistoryTracker mHistoryTracker;
+#endif
+
 #if (OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE || OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE) && OPENTHREAD_FTD
     MeshCoP::DatasetUpdater mDatasetUpdater;
 #endif
@@ -828,6 +835,13 @@
 }
 #endif
 
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+template <> inline Utils::HistoryTracker &Instance::Get(void)
+{
+    return mHistoryTracker;
+}
+#endif
+
 #if (OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE || OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE) && OPENTHREAD_FTD
 template <> inline MeshCoP::DatasetUpdater &Instance::Get(void)
 {
diff --git a/src/core/common/notifier.cpp b/src/core/common/notifier.cpp
index f281145..a77660a 100644
--- a/src/core/common/notifier.cpp
+++ b/src/core/common/notifier.cpp
@@ -177,6 +177,9 @@
 #if OPENTHREAD_CONFIG_OTNS_ENABLE
     Get<Utils::Otns>().HandleNotifierEvents(events);
 #endif
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+    Get<Utils::HistoryTracker>().HandleNotifierEvents(events);
+#endif
 #if OPENTHREAD_ENABLE_VENDOR_EXTENSION
     Get<Extension::ExtensionBase>().HandleNotifierEvents(events);
 #endif
diff --git a/src/core/config/history_tracker.h b/src/core/config/history_tracker.h
new file mode 100644
index 0000000..963c912
--- /dev/null
+++ b/src/core/config/history_tracker.h
@@ -0,0 +1,94 @@
+/*
+ *  Copyright (c) 2021, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file includes compile-time configurations for History Tracker module.
+ *
+ */
+
+#ifndef CONFIG_HISTORY_TRACKER_H_
+#define CONFIG_HISTORY_TRACKER_H_
+
+/**
+ * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+ *
+ * Define as 1 to enable History Tracker module.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+#define OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE 0
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE
+ *
+ * Specifies the maximum number of entries in Network Info (role, mode, partition ID, RLOC16) history list.
+ *
+ * Can be set to zero to configure History Tracker module not to collect any entries.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE
+#define OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE 32
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_RX_LIST_SIZE
+ *
+ * Specifies the maximum number of entries in RX history list.
+ *
+ * Can be set to zero to configure History Tracker module not to collect any RX history.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_RX_LIST_SIZE
+#define OPENTHREAD_CONFIG_HISTORY_TRACKER_RX_LIST_SIZE 32
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_TX_LIST_SIZE
+ *
+ * Specifies the maximum number of entries in TX history list.
+ *
+ * Can be set to zero to configure History Tracker module not to collect any TX history.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_TX_LIST_SIZE
+#define OPENTHREAD_CONFIG_HISTORY_TRACKER_TX_LIST_SIZE 32
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_EXCLUDE_THREAD_CONTROL_MESSAGES
+ *
+ * Define as 1 to exclude Thread Control message (e.g., MLE, TMF) from TX and RX history.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_EXCLUDE_THREAD_CONTROL_MESSAGES
+#define OPENTHREAD_CONFIG_HISTORY_TRACKER_EXCLUDE_THREAD_CONTROL_MESSAGES 1
+#endif
+
+#endif // CONFIG_HISTORY_TRACKER_H_
diff --git a/src/core/net/ip6.hpp b/src/core/net/ip6.hpp
index 63ff981..0789b49 100644
--- a/src/core/net/ip6.hpp
+++ b/src/core/net/ip6.hpp
@@ -298,9 +298,11 @@
     const PriorityQueue &GetSendQueue(void) const { return mSendQueue; }
 
     /**
-     * This static method converts an `IpProto` enumeration to a string.
+     * This static method converts an IP protocol number to a string.
      *
-     * @returns The string representation of an IP protocol enumeration.
+     * @param[in] aIpPorto  An IP protocol number.
+     *
+     * @returns The string representation of @p aIpProto.
      *
      */
     static const char *IpProtoToString(uint8_t aIpProto);
diff --git a/src/core/net/ip6_headers.hpp b/src/core/net/ip6_headers.hpp
index efe90cf..2ddc219 100644
--- a/src/core/net/ip6_headers.hpp
+++ b/src/core/net/ip6_headers.hpp
@@ -86,15 +86,15 @@
  */
 
 // Internet Protocol Numbers
-static constexpr uint8_t kProtoHopOpts  = 0;  ///< IPv6 Hop-by-Hop Option
-static constexpr uint8_t kProtoTcp      = 6;  ///< Transmission Control Protocol
-static constexpr uint8_t kProtoUdp      = 17; ///< User Datagram
-static constexpr uint8_t kProtoIp6      = 41; ///< IPv6 encapsulation
-static constexpr uint8_t kProtoRouting  = 43; ///< Routing Header for IPv6
-static constexpr uint8_t kProtoFragment = 44; ///< Fragment Header for IPv6
-static constexpr uint8_t kProtoIcmp6    = 58; ///< ICMP for IPv6
-static constexpr uint8_t kProtoNone     = 59; ///< No Next Header for IPv6
-static constexpr uint8_t kProtoDstOpts  = 60; ///< Destination Options for IPv6
+static constexpr uint8_t kProtoHopOpts  = OT_IP6_PROTO_HOP_OPTS; ///< IPv6 Hop-by-Hop Option
+static constexpr uint8_t kProtoTcp      = OT_IP6_PROTO_TCP;      ///< Transmission Control Protocol
+static constexpr uint8_t kProtoUdp      = OT_IP6_PROTO_UDP;      ///< User Datagram
+static constexpr uint8_t kProtoIp6      = OT_IP6_PROTO_IP6;      ///< IPv6 encapsulation
+static constexpr uint8_t kProtoRouting  = OT_IP6_PROTO_ROUTING;  ///< Routing Header for IPv6
+static constexpr uint8_t kProtoFragment = OT_IP6_PROTO_FRAGMENT; ///< Fragment Header for IPv6
+static constexpr uint8_t kProtoIcmp6    = OT_IP6_PROTO_ICMP6;    ///< ICMP for IPv6
+static constexpr uint8_t kProtoNone     = OT_IP6_PROTO_NONE;     ///< No Next Header for IPv6
+static constexpr uint8_t kProtoDstOpts  = OT_IP6_PROTO_DST_OPTS; ///< Destination Options for IPv6
 
 /**
  * Class Selectors
diff --git a/src/core/openthread-core-config.h b/src/core/openthread-core-config.h
index 423b35a..0e9e714 100644
--- a/src/core/openthread-core-config.h
+++ b/src/core/openthread-core-config.h
@@ -71,6 +71,7 @@
 #include "config/dns_client.h"
 #include "config/dnssd_server.h"
 #include "config/dtls.h"
+#include "config/history_tracker.h"
 #include "config/ip6.h"
 #include "config/joiner.h"
 #include "config/link_quality.h"
diff --git a/src/core/thread/mesh_forwarder.cpp b/src/core/thread/mesh_forwarder.cpp
index dcca5f2..c77b05b 100644
--- a/src/core/thread/mesh_forwarder.cpp
+++ b/src/core/thread/mesh_forwarder.cpp
@@ -1042,6 +1042,10 @@
     }
 #endif
 
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+    Get<Utils::HistoryTracker>().RecordTxMessage(*mSendMessage, aMacDest);
+#endif
+
     LogMessage(kMessageTransmit, *mSendMessage, &aMacDest, txError);
 
     if (mSendMessage->GetType() == Message::kTypeIp6)
@@ -1441,6 +1445,10 @@
 {
     ThreadNetif &netif = Get<ThreadNetif>();
 
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+    Get<Utils::HistoryTracker>().RecordRxMessage(aMessage, aMacSource);
+#endif
+
     LogMessage(kMessageReceive, aMessage, &aMacSource, kErrorNone);
 
     if (aMessage.GetType() == Message::kTypeIp6)
@@ -1620,8 +1628,6 @@
 
 // LCOV_EXCL_START
 
-#if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_NOTE) && (OPENTHREAD_CONFIG_LOG_MAC == 1)
-
 Error MeshForwarder::ParseIp6UdpTcpHeader(const Message &aMessage,
                                           Ip6::Header &  aIp6Header,
                                           uint16_t &     aChecksum,
@@ -1668,6 +1674,8 @@
     return error;
 }
 
+#if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_NOTE) && (OPENTHREAD_CONFIG_LOG_MAC == 1)
+
 const char *MeshForwarder::MessageActionToString(MessageAction aAction, Error aError)
 {
     static const char *const kMessageActionStrings[] = {
diff --git a/src/core/thread/mesh_forwarder.hpp b/src/core/thread/mesh_forwarder.hpp
index 255b3bc..7d288af 100644
--- a/src/core/thread/mesh_forwarder.hpp
+++ b/src/core/thread/mesh_forwarder.hpp
@@ -58,6 +58,10 @@
 class DiscoverScanner;
 }
 
+namespace Utils {
+class HistoryTracker;
+}
+
 /**
  * @addtogroup core-mesh-forwarding
  *
@@ -154,6 +158,7 @@
     friend class IndirectSender;
     friend class Mle::DiscoverScanner;
     friend class TimeTicker;
+    friend class Utils::HistoryTracker;
 
 public:
     /**
@@ -503,15 +508,16 @@
                               const Mac::Address &aMacDest,
                               bool                aIsSecure);
 
+    static Error ParseIp6UdpTcpHeader(const Message &aMessage,
+                                      Ip6::Header &  aIp6Header,
+                                      uint16_t &     aChecksum,
+                                      uint16_t &     aSourcePort,
+                                      uint16_t &     aDestPort);
+
 #if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_NOTE) && (OPENTHREAD_CONFIG_LOG_MAC == 1)
     const char *MessageActionToString(MessageAction aAction, Error aError);
     const char *MessagePriorityToString(const Message &aMessage);
 
-    Error ParseIp6UdpTcpHeader(const Message &aMessage,
-                               Ip6::Header &  aIp6Header,
-                               uint16_t &     aChecksum,
-                               uint16_t &     aSourcePort,
-                               uint16_t &     aDestPort);
 #if OPENTHREAD_FTD
     Error DecompressIp6UdpTcpHeader(const Message &     aMessage,
                                     uint16_t            aOffset,
diff --git a/src/core/thread/mle.cpp b/src/core/thread/mle.cpp
index 8a98c8a..dc7bce5 100644
--- a/src/core/thread/mle.cpp
+++ b/src/core/thread/mle.cpp
@@ -748,6 +748,10 @@
     VerifyOrExit(mDeviceMode != aDeviceMode);
     mDeviceMode = aDeviceMode;
 
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+    Get<Utils::HistoryTracker>().RecordNetworkInfo();
+#endif
+
 #if OPENTHREAD_CONFIG_OTNS_ENABLE
     Get<Utils::Otns>().EmitDeviceMode(mDeviceMode);
 #endif
@@ -4406,11 +4410,11 @@
 const char *Mle::RoleToString(DeviceRole aRole)
 {
     static const char *const kRoleStrings[] = {
-        "Disabled", // (0) kRoleDisabled
-        "Detached", // (1) kRoleDetached
-        "Child",    // (2) kRoleChild
-        "Router",   // (3) kRoleRouter
-        "Leader",   // (4) kRoleLeader
+        "disabled", // (0) kRoleDisabled
+        "detached", // (1) kRoleDetached
+        "child",    // (2) kRoleChild
+        "router",   // (3) kRoleRouter
+        "leader",   // (4) kRoleLeader
     };
 
     static_assert(kRoleDisabled == 0, "kRoleDisabled value is incorrect");
@@ -4419,7 +4423,7 @@
     static_assert(kRoleRouter == 3, "kRoleRouter value is incorrect");
     static_assert(kRoleLeader == 4, "kRoleLeader value is incorrect");
 
-    return kRoleStrings[aRole];
+    return (aRole <= OT_ARRAY_LENGTH(kRoleStrings)) ? kRoleStrings[aRole] : "invalid";
 }
 
 // LCOV_EXCL_START
diff --git a/src/core/utils/history_tracker.cpp b/src/core/utils/history_tracker.cpp
new file mode 100644
index 0000000..3414220
--- /dev/null
+++ b/src/core/utils/history_tracker.cpp
@@ -0,0 +1,363 @@
+/*
+ *  Copyright (c) 2021, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file implements the History Tracker module.
+ */
+
+#include "history_tracker.hpp"
+
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+
+#include "common/code_utils.hpp"
+#include "common/debug.hpp"
+#include "common/instance.hpp"
+#include "common/locator_getters.hpp"
+#include "common/string.hpp"
+#include "common/timer.hpp"
+
+namespace ot {
+namespace Utils {
+
+//---------------------------------------------------------------------------------------------------------------------
+// HistoryTracker
+
+HistoryTracker::HistoryTracker(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mTimer(aInstance, HandleTimer)
+{
+    mTimer.Start(kAgeCheckPeriod);
+}
+
+void HistoryTracker::RecordNetworkInfo(void)
+{
+    NetworkInfo *   entry = mNetInfoHistory.AddNewEntry();
+    Mle::DeviceMode mode;
+
+    VerifyOrExit(entry != nullptr);
+
+    entry->mRole        = static_cast<otDeviceRole>(Get<Mle::Mle>().GetRole());
+    entry->mRloc16      = Get<Mle::Mle>().GetRloc16();
+    entry->mPartitionId = Get<Mle::Mle>().GetLeaderData().GetPartitionId();
+    mode                = Get<Mle::Mle>().GetDeviceMode();
+    mode.Get(entry->mMode);
+
+exit:
+    return;
+}
+
+void HistoryTracker::RecordMessage(const Message &aMessage, const Mac::Address &aMacAddresss, MessageType aType)
+{
+    MessageInfo *     entry = nullptr;
+    Ip6::Header       ip6Header;
+    Ip6::Icmp::Header icmp6Header;
+    uint8_t           ip6Proto;
+    uint16_t          checksum;
+    uint16_t          sourcePort;
+    uint16_t          destPort;
+
+    VerifyOrExit(aMessage.GetType() == Message::kTypeIp6);
+
+    SuccessOrExit(MeshForwarder::ParseIp6UdpTcpHeader(aMessage, ip6Header, checksum, sourcePort, destPort));
+
+    ip6Proto = ip6Header.GetNextHeader();
+
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_EXCLUDE_THREAD_CONTROL_MESSAGES
+    if (ip6Proto == Ip6::kProtoUdp)
+    {
+        uint16_t port = 0;
+
+        switch (aType)
+        {
+        case kRxMessage:
+            port = destPort;
+            break;
+
+        case kTxMessage:
+            port = sourcePort;
+            break;
+        }
+
+        VerifyOrExit((port != Mle::kUdpPort) && (port != Tmf::kUdpPort));
+    }
+#endif
+
+    if (ip6Proto == Ip6::kProtoIcmp6)
+    {
+        SuccessOrExit(aMessage.Read(sizeof(Ip6::Header), icmp6Header));
+        checksum = icmp6Header.GetChecksum();
+    }
+    else
+    {
+        icmp6Header.Clear();
+    }
+
+    switch (aType)
+    {
+    case kRxMessage:
+        entry = mRxHistory.AddNewEntry();
+        break;
+
+    case kTxMessage:
+        entry = mTxHistory.AddNewEntry();
+        break;
+    }
+
+    VerifyOrExit(entry != nullptr);
+
+    entry->mPayloadLength        = ip6Header.GetPayloadLength();
+    entry->mNeighborRloc16       = aMacAddresss.IsShort() ? aMacAddresss.GetShort() : kInvalidRloc16;
+    entry->mSource.mAddress      = ip6Header.GetSource();
+    entry->mSource.mPort         = sourcePort;
+    entry->mDestination.mAddress = ip6Header.GetDestination();
+    entry->mDestination.mPort    = destPort;
+    entry->mChecksum             = checksum;
+    entry->mIpProto              = ip6Proto;
+    entry->mIcmp6Type            = icmp6Header.GetType();
+    entry->mAveRxRss             = (aType == kRxMessage) ? aMessage.GetRssAverager().GetAverage() : kInvalidRss;
+    entry->mLinkSecurity         = aMessage.IsLinkSecurityEnabled();
+    entry->mTxSuccess            = (aType == kTxMessage) ? aMessage.GetTxSuccess() : true;
+    entry->mPriority             = aMessage.GetPriority();
+
+    if (aMacAddresss.IsExtended())
+    {
+        Neighbor *neighbor = Get<NeighborTable>().FindNeighbor(aMacAddresss, Neighbor::kInStateAnyExceptInvalid);
+
+        if (neighbor != nullptr)
+        {
+            entry->mNeighborRloc16 = neighbor->GetRloc16();
+        }
+    }
+
+#if OPENTHREAD_CONFIG_MULTI_RADIO
+    if (aMessage.IsRadioTypeSet())
+    {
+        switch (aMessage.GetRadioType())
+        {
+#if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
+        case Mac::kRadioTypeIeee802154:
+            entry->mRadioIeee802154 = true;
+            break;
+#endif
+#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
+        case Mac::kRadioTypeTrel:
+            entry->mRadioTrelUdp6 = true;
+            break;
+#endif
+        }
+
+        // Radio type may not be set on a tx message indicating that it
+        // was sent over all radio types (e.g., for broadcast frame).
+        // In such a case, we set all supported radios from `else`
+        // block below.
+    }
+    else
+#endif // OPENTHREAD_CONFIG_MULTI_RADIO
+    {
+#if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
+        entry->mRadioIeee802154 = true;
+#endif
+
+#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
+        entry->mRadioTrelUdp6 = true;
+#endif
+    }
+
+exit:
+    return;
+}
+
+void HistoryTracker::HandleNotifierEvents(Events aEvents)
+{
+    if (aEvents.ContainsAny(kEventThreadRoleChanged | kEventThreadRlocAdded | kEventThreadRlocRemoved |
+                            kEventThreadPartitionIdChanged))
+    {
+        RecordNetworkInfo();
+    }
+}
+
+void HistoryTracker::HandleTimer(Timer &aTimer)
+{
+    aTimer.Get<HistoryTracker>().HandleTimer();
+}
+
+void HistoryTracker::HandleTimer(void)
+{
+    mNetInfoHistory.UpdateAgedEntries();
+    mRxHistory.UpdateAgedEntries();
+    mTxHistory.UpdateAgedEntries();
+
+    mTimer.Start(kAgeCheckPeriod);
+}
+
+void HistoryTracker::EntryAgeToString(uint32_t aEntryAge, char *aBuffer, uint16_t aSize)
+{
+    StringWriter writer(aBuffer, aSize);
+
+    if (aEntryAge >= kMaxAge)
+    {
+        writer.Append("more than %u days", kMaxAge / kOneDayInMsec);
+    }
+    else
+    {
+        uint32_t days = aEntryAge / kOneDayInMsec;
+
+        if (days > 0)
+        {
+            writer.Append("%u day%s ", days, (days == 1) ? "" : "s");
+            aEntryAge -= days * kOneDayInMsec;
+        }
+
+        writer.Append("%02u:%02u:%02u.%03u", (aEntryAge / kOneHourInMsec),
+                      (aEntryAge % kOneDayInMsec) / kOneMinuteInMsec, (aEntryAge % kOneMinuteInMsec) / kOneSecondInMsec,
+                      (aEntryAge % kOneSecondInMsec));
+    }
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// HistoryTracker::Timestamp
+
+void HistoryTracker::Timestamp::SetToNow(void)
+{
+    mTime = TimerMilli::GetNow();
+
+    // If the current time happens to be the special value which we
+    // use to indicate "distant past", decrement the time by one.
+
+    if (mTime.GetValue() == kDistantPast)
+    {
+        mTime.SetValue(mTime.GetValue() - 1);
+    }
+}
+
+uint32_t HistoryTracker::Timestamp::GetDurationTill(TimeMilli aTime) const
+{
+    return IsDistantPast() ? kMaxAge : OT_MIN(aTime - mTime, kMaxAge);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// HistoryTracker::List
+
+HistoryTracker::List::List(void)
+    : mStartIndex(0)
+    , mSize(0)
+{
+}
+
+void HistoryTracker::List::Clear(void)
+{
+    mStartIndex = 0;
+    mSize       = 0;
+}
+
+uint16_t HistoryTracker::List::Add(uint16_t aMaxSize, Timestamp aTimestamps[])
+{
+    // Add a new entry and return its list index. Overwrites the
+    // oldest entry if list is full.
+    //
+    // Entries are saved in the order they are added such that
+    // `mStartIndex` is the newest entry and the entries after up
+    // to `mSize` are the previously added entries.
+
+    mStartIndex = (mStartIndex == 0) ? aMaxSize - 1 : mStartIndex - 1;
+    mSize += (mSize == aMaxSize) ? 0 : 1;
+
+    aTimestamps[mStartIndex].SetToNow();
+
+    return mStartIndex;
+}
+
+Error HistoryTracker::List::Iterate(uint16_t        aMaxSize,
+                                    const Timestamp aTimestamps[],
+                                    Iterator &      aIterator,
+                                    uint16_t &      aListIndex,
+                                    uint32_t &      aEntryAge) const
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(aIterator.GetEntryNumber() < mSize, error = kErrorNotFound);
+
+    aListIndex = MapEntryNumberToListIndex(aIterator.GetEntryNumber(), aMaxSize);
+    aEntryAge  = aTimestamps[aListIndex].GetDurationTill(aIterator.GetInitTime());
+
+    aIterator.IncrementEntryNumber();
+
+exit:
+    return error;
+}
+
+uint16_t HistoryTracker::List::MapEntryNumberToListIndex(uint16_t aEntryNumber, uint16_t aMaxSize) const
+{
+    // Map the `aEntryNumber` to the list index. `aEntryNumber` value
+    // of zero corresponds to the newest (the most recently added)
+    // entry and value one to next one and so on. List index
+    // warps at the end of array to start of array. Caller MUST
+    // ensure `aEntryNumber` is smaller than `mSize`.
+
+    uint32_t index;
+
+    OT_ASSERT(aEntryNumber < mSize);
+
+    index = static_cast<uint32_t>(aEntryNumber) + mStartIndex;
+    index -= (index >= aMaxSize) ? aMaxSize : 0;
+
+    return static_cast<uint16_t>(index);
+}
+
+void HistoryTracker::List::UpdateAgedEntries(uint16_t aMaxSize, Timestamp aTimestamps[])
+{
+    TimeMilli now = TimerMilli::GetNow();
+
+    // We go through the entries in reverse (starting with the oldest
+    // entry) and check if the entry's age is larger than `kMaxAge`
+    // and if so mark it as "distant past". We can stop as soon as we
+    // get to an entry with age smaller than max.
+    //
+    // The `for()` loop condition is `(entryNumber < mSize)` which
+    // ensures that we go through the loop body for `entryNumber`
+    // value of zero and then in the next iteration (when the
+    // `entryNumber` rolls over) we stop.
+
+    for (uint16_t entryNumber = mSize - 1; entryNumber < mSize; entryNumber--)
+    {
+        uint16_t index = MapEntryNumberToListIndex(entryNumber, aMaxSize);
+
+        if (aTimestamps[index].GetDurationTill(now) < kMaxAge)
+        {
+            break;
+        }
+
+        aTimestamps[index].MarkAsDistantPast();
+    }
+}
+
+} // namespace Utils
+} // namespace ot
+
+#endif // #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
diff --git a/src/core/utils/history_tracker.hpp b/src/core/utils/history_tracker.hpp
new file mode 100644
index 0000000..96f3d76
--- /dev/null
+++ b/src/core/utils/history_tracker.hpp
@@ -0,0 +1,319 @@
+/*
+ *  Copyright (c) 2021, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file includes definitions to support History Tracker module.
+ */
+
+#ifndef HISTORY_TRACKER_HPP_
+#define HISTORY_TRACKER_HPP_
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+
+#include <openthread/history_tracker.h>
+#include <openthread/platform/radio.h>
+
+#include "common/clearable.hpp"
+#include "common/locator.hpp"
+#include "common/non_copyable.hpp"
+#include "common/notifier.hpp"
+#include "common/timer.hpp"
+#include "net/socket.hpp"
+#include "thread/mesh_forwarder.hpp"
+#include "thread/mle.hpp"
+#include "thread/mle_types.hpp"
+
+namespace ot {
+namespace Utils {
+
+/**
+ * This class implements History Tracker.
+ *
+ */
+class HistoryTracker : public InstanceLocator, private NonCopyable
+{
+    friend class ot::MeshForwarder;
+    friend class ot::Notifier;
+    friend class ot::Mle::Mle;
+
+public:
+    /**
+     * This constant specifies the maximum age of entries which is 49 days (value in msec).
+     *
+     * Entries older than the max age will give this value as their age.
+     *
+     */
+    static constexpr uint32_t kMaxAge = OT_HISTORY_TRACKER_MAX_AGE;
+
+    /**
+     * This constant specifies the recommend string size to represent an entry age
+     *
+     */
+    static constexpr uint16_t kEntryAgeStringSize = OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE;
+
+    /**
+     * This type represents an iterator to iterate through a history list.
+     *
+     */
+    class Iterator : public otHistoryTrackerIterator
+    {
+        friend class HistoryTracker;
+
+    public:
+        /**
+         * This method initializes an `Iterator`
+         *
+         * An iterator MUST be initialized before it is used. An iterator can be initialized again to start from
+         * the beginning of the list.
+         *
+         */
+        void Init(void) { ResetEntryNumber(), SetInitTime(); }
+
+    private:
+        uint16_t  GetEntryNumber(void) const { return mData16; }
+        void      ResetEntryNumber(void) { mData16 = 0; }
+        void      IncrementEntryNumber(void) { mData16++; }
+        TimeMilli GetInitTime(void) const { return TimeMilli(mData32); }
+        void      SetInitTime(void) { mData32 = TimerMilli::GetNow().GetValue(); }
+    };
+
+    /**
+     * This type represents Thread network info.
+     *
+     */
+    typedef otHistoryTrackerNetworkInfo NetworkInfo;
+
+    /**
+     * This type represents a RX/TX IPv6 message info.
+     *
+     */
+    typedef otHistoryTrackerMessageInfo MessageInfo;
+
+    /**
+     * This constructor initializes the `HistoryTracker`.
+     *
+     * @param[in]  aInstance     A reference to the OpenThread instance.
+     *
+     */
+    explicit HistoryTracker(Instance &aInstance);
+
+    /**
+     * This method iterates over the entries in the network info history list.
+     *
+     * @param[inout] aIterator  An iterator. MUST be initialized.
+     * @param[out]   aEntryAge  A reference to a variable to output the entry's age.
+     *                          Age is provided as the duration (in milliseconds) from when entry was recorded to
+     *                          @p aIterator initialization time. It is set to `kMaxAge` for entries older than max age.
+     *
+     * @returns A pointer to `NetworkInfo` entry or `nullptr` if no more entries in the list.
+     *
+     */
+    const NetworkInfo *IterateNetInfoHistory(Iterator &aIterator, uint32_t &aEntryAge) const
+    {
+        return mNetInfoHistory.Iterate(aIterator, aEntryAge);
+    }
+
+    /**
+     * This method iterates over the entries in the RX history list.
+     *
+     * @param[inout] aIterator  An iterator. MUST be initialized.
+     * @param[out]   aEntryAge  A reference to a variable to output the entry's age.
+     *                          Age is provided as the duration (in milliseconds) from when entry was recorded to
+     *                          @p aIterator initialization time. It is set to `kMaxAge` for entries older than max age.
+     *
+     * @returns A pointer to `MessageInfo` entry or `nullptr` if no more entries in the list.
+     *
+     */
+    const MessageInfo *IterateRxHistory(Iterator &aIterator, uint32_t &aEntryAge) const
+    {
+        return mRxHistory.Iterate(aIterator, aEntryAge);
+    }
+
+    /**
+     * This method iterates over the entries in the TX history list.
+     *
+     * @param[inout] aIterator  An iterator. MUST be initialized.
+     * @param[out]   aEntryAge  A reference to a variable to output the entry's age.
+     *                          Age is provided as the duration (in milliseconds) from when entry was recorded to
+     *                          @p aIterator initialization time. It is set to `kMaxAge` for entries older than max age.
+     *
+     * @returns A pointer to `MessageInfo` entry or `nullptr` if no more entries in the list.
+     *
+     */
+    const MessageInfo *IterateTxHistory(Iterator &aIterator, uint32_t &aEntryAge) const
+    {
+        return mTxHistory.Iterate(aIterator, aEntryAge);
+    }
+
+    /**
+     * This static method converts a given entry age to a human-readable string.
+     *
+     * The entry age string follows the format "<hh>:<mm>:<ss>.<mmmm>" for hours, minutes, seconds and millisecond
+     * (if shorter than one day) or "<dd> days <hh>:<mm>:<ss>.<mmmm>" (if longer than one day).
+     *
+     * If the resulting string does not fit in @p aBuffer (within its @p aSize characters), the string will be
+     * truncated but the outputted string is always null-terminated.
+     *
+     * @param[in]  aEntryAge The entry age (duration in msec).
+     * @param[out] aBuffer   A pointer to a char array to output the string (MUST NOT be NULL).
+     * @param[in]  aSize     The size of @p aBuffer (in bytes). Recommended to use `OT_IP6_ADDRESS_STRING_SIZE`.
+     *
+     */
+    static void EntryAgeToString(uint32_t aEntryAge, char *aBuffer, uint16_t aSize);
+
+private:
+    static constexpr uint32_t kOneSecondInMsec = 1000;
+    static constexpr uint32_t kOneMinuteInMsec = 60 * kOneSecondInMsec;
+    static constexpr uint32_t kOneHourInMsec   = 60 * kOneMinuteInMsec;
+    static constexpr uint32_t kOneDayInMsec    = 24 * kOneHourInMsec;
+
+    // `Timestamp` uses `uint32_t` value. `2^32` msec is 49 days, 17
+    // hours, 2 minutes and 47 seconds and 296 msec. We use 49 days
+    // as `kMaxAge` and check for aged entries every 16 hours.
+
+    static constexpr uint32_t kAgeCheckPeriod = 16 * kOneHourInMsec;
+
+    static constexpr uint16_t kNetInfoListSize = OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE;
+    static constexpr uint16_t kRxListSize      = OPENTHREAD_CONFIG_HISTORY_TRACKER_RX_LIST_SIZE;
+    static constexpr uint16_t kTxListSize      = OPENTHREAD_CONFIG_HISTORY_TRACKER_TX_LIST_SIZE;
+
+    static constexpr int8_t   kInvalidRss    = OT_RADIO_RSSI_INVALID;
+    static constexpr uint16_t kInvalidRloc16 = Mac::kShortAddrInvalid;
+
+    class Timestamp
+    {
+    public:
+        void     SetToNow(void);
+        uint32_t GetDurationTill(TimeMilli aTime) const;
+        bool     IsDistantPast(void) const { return (mTime.GetValue() == kDistantPast); }
+        void     MarkAsDistantPast(void) { return mTime.SetValue(kDistantPast); }
+
+    private:
+        static constexpr uint32_t kDistantPast = 0;
+
+        TimeMilli mTime;
+    };
+
+    // An ordered list of timestamped items (base class of `EntryList<Entry, kSize>`).
+    class List : private NonCopyable
+    {
+    public:
+        void     Clear(void);
+        uint16_t GetSize(void) const { return mSize; }
+
+    protected:
+        List(void);
+        uint16_t Add(uint16_t aMaxSize, Timestamp aTimestamps[]);
+        void     UpdateAgedEntries(uint16_t aMaxSize, Timestamp aTimestamps[]);
+        uint16_t MapEntryNumberToListIndex(uint16_t aEntryNumber, uint16_t aMaxSize) const;
+        Error    Iterate(uint16_t        aMaxSize,
+                         const Timestamp aTimestamps[],
+                         Iterator &      aIterator,
+                         uint16_t &      aListIndex,
+                         uint32_t &      aEntryAge) const;
+
+    private:
+        uint16_t mStartIndex;
+        uint16_t mSize;
+    };
+
+    // A history list (with given max size) of timestamped `Entry` items.
+    template <typename Entry, uint16_t kMaxSize> class EntryList : public List
+    {
+    public:
+        // Adds a new entry to the list or overwrites the oldest entry
+        // if list is full. First version returns a pointer to the
+        // new `Entry` (for caller to populate). Second version copies
+        // the given `aEntry`.
+        Entry *AddNewEntry(void) { return &mEntries[Add(kMaxSize, mTimestamps)]; }
+        void   AddNewEntry(const Entry &aEntry) { mEntries[Add(kMaxSize, mTimestamps)] = aEntry; }
+
+        void UpdateAgedEntries(void) { List::UpdateAgedEntries(kMaxSize, mTimestamps); }
+
+        const Entry *Iterate(Iterator &aIterator, uint32_t &aEntryAge) const
+        {
+            uint16_t index;
+
+            return (List::Iterate(kMaxSize, mTimestamps, aIterator, index, aEntryAge) == kErrorNone) ? &mEntries[index]
+                                                                                                     : nullptr;
+        }
+
+    private:
+        Timestamp mTimestamps[kMaxSize];
+        Entry     mEntries[kMaxSize];
+    };
+
+    // Partial specialization for `kMaxSize` zero.
+    template <typename Entry> class EntryList<Entry, 0> : private NonCopyable
+    {
+    public:
+        void         Clear(void) {}
+        uint16_t     GetSize(void) const { return 0; }
+        Entry *      AddNewEntry(void) { return nullptr; }
+        void         AddNewEntry(const Entry &) {}
+        const Entry *Iterate(Iterator &, uint32_t &) const { return nullptr; }
+        void         RemoveAgedEntries(void) {}
+    };
+
+    enum MessageType : uint8_t
+    {
+        kRxMessage,
+        kTxMessage,
+    };
+
+    void RecordRxMessage(const Message &aMessage, const Mac::Address &aMacSource)
+    {
+        RecordMessage(aMessage, aMacSource, kRxMessage);
+    }
+
+    void RecordTxMessage(const Message &aMessage, const Mac::Address &aMacDest)
+    {
+        RecordMessage(aMessage, aMacDest, kTxMessage);
+    }
+
+    void        RecordNetworkInfo(void);
+    void        RecordMessage(const Message &aMessage, const Mac::Address &aMacAddress, MessageType aType);
+    void        HandleNotifierEvents(Events aEvents);
+    static void HandleTimer(Timer &aTimer);
+    void        HandleTimer(void);
+
+    EntryList<NetworkInfo, kNetInfoListSize> mNetInfoHistory;
+    EntryList<MessageInfo, kRxListSize>      mRxHistory;
+    EntryList<MessageInfo, kTxListSize>      mTxHistory;
+    TimerMilli                               mTimer;
+};
+
+} // namespace Utils
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+
+#endif // HISTORY_TRACKER_HPP_
diff --git a/src/posix/Makefile-posix b/src/posix/Makefile-posix
index 9d37ea1..07a1f43 100644
--- a/src/posix/Makefile-posix
+++ b/src/posix/Makefile-posix
@@ -53,6 +53,7 @@
 DNSSD_SERVER                         ?= 1
 DYNAMIC_LOG_LEVEL                    ?= 1
 ECDSA                                ?= 1
+HISTORY_TRACKER                      ?= 1
 IP6_FRAGM                            ?= 1
 JAM_DETECTION                        ?= 1
 JOINER                               ?= 1
diff --git a/src/posix/platform/openthread-core-posix-config.h b/src/posix/platform/openthread-core-posix-config.h
index 35c13a7..d1ceb92 100644
--- a/src/posix/platform/openthread-core-posix-config.h
+++ b/src/posix/platform/openthread-core-posix-config.h
@@ -218,6 +218,16 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+ *
+ * Define as 1 to enable History Tracker module.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+#define OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE 1
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_HEAP_INTERNAL_SIZE
  *
  * The size of heap buffer when DTLS is enabled.
diff --git a/tests/fuzz/oss-fuzz-build b/tests/fuzz/oss-fuzz-build
index 37b4291..7b21036 100755
--- a/tests/fuzz/oss-fuzz-build
+++ b/tests/fuzz/oss-fuzz-build
@@ -54,6 +54,7 @@
         -DOT_DHCP6_SERVER=ON \
         -DOT_DNS_CLIENT=ON \
         -DOT_ECDSA=ON \
+        -DOT_HISTORY_TRACKER=ON \
         -DOT_IP6_FRAGM=ON \
         -DOT_JAM_DETECTION=ON \
         -DOT_JOINER=ON \
diff --git a/tests/scripts/thread-cert/Makefile.am b/tests/scripts/thread-cert/Makefile.am
index f873f33..1a94f57 100644
--- a/tests/scripts/thread-cert/Makefile.am
+++ b/tests/scripts/thread-cert/Makefile.am
@@ -162,6 +162,7 @@
     test_diag.py                                                     \
     test_dns_client_config_auto_start.py                             \
     test_dnssd.py                                                    \
+    test_history_tracker.py                                          \
     test_ipv6.py                                                     \
     test_ipv6_fragmentation.py                                       \
     test_ipv6_source_selection.py                                    \
@@ -228,6 +229,7 @@
     test_diag.py                                                     \
     test_dns_client_config_auto_start.py                             \
     test_dnssd.py                                                    \
+    test_history_tracker.py                                          \
     test_ipv6.py                                                     \
     test_ipv6_fragmentation.py                                       \
     test_ipv6_source_selection.py                                    \
diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py
index 04cdbb1..61a97e8 100755
--- a/tests/scripts/thread-cert/node.py
+++ b/tests/scripts/thread-cert/node.py
@@ -1331,6 +1331,10 @@
         self.send_command(cmd)
         self._expect_done()
 
+    def get_partition_id(self):
+        self.send_command('partitionid')
+        return self._expect_result(r'\d+')
+
     def get_preferred_partition_id(self):
         self.send_command('partitionid preferred')
         return self._expect_result(r'\d+')
@@ -2720,6 +2724,112 @@
         self.send_command(cmd)
         self._expect_command_output(cmd)
 
+    def history_netinfo(self, num_entries=0):
+        """
+        Get the `netinfo` history list, parse each entry and return
+        a list of dictionary (string key and string value) entries.
+
+        Example of return value:
+        [
+            {
+                'age': '00:00:00.000 ago',
+                'role': 'disabled',
+                'mode': 'rdn',
+                'rloc16': '0x7400',
+                'partition-id': '1318093703'
+            },
+            {
+                'age': '00:00:02.588 ago',
+                'role': 'leader',
+                'mode': 'rdn',
+                'rloc16': '0x7400',
+                'partition-id': '1318093703'
+            }
+        ]
+        """
+        cmd = f'history netinfo list {num_entries}'
+        self.send_command(cmd)
+        output = self._expect_command_output(cmd)
+        netinfos = []
+        for entry in output:
+            netinfo = {}
+            age, info = entry.split(' -> ')
+            netinfo['age'] = age
+            for item in info.split(' '):
+                k, v = item.split(':')
+                netinfo[k] = v
+            netinfos.append(netinfo)
+        return netinfos
+
+    def history_rx(self, num_entries=0):
+        """
+        Get the IPv6 RX history list, parse each entry and return
+        a list of dictionary (string key and string value) entries.
+
+        Example of return value:
+        [
+            {
+                'age': '00:00:01.999',
+                'type': 'ICMP6(EchoReqst)',
+                'len': '16',
+                'sec': 'yes',
+                'prio': 'norm',
+                'rss': '-20',
+                'from': '0xac00',
+                'radio': '15.4',
+                'src': '[fd00:db8:0:0:2cfa:fd61:58a9:f0aa]:0',
+                'dst': '[fd00:db8:0:0:ed7e:2d04:e543:eba5]:0',
+            }
+        ]
+        """
+        cmd = f'history rx list {num_entries}'
+        self.send_command(cmd)
+        return self._parse_history_rx_tx_ouput(self._expect_command_output(cmd))
+
+    def history_tx(self, num_entries=0):
+        """
+        Get the IPv6 TX history list, parse each entry and return
+        a list of dictionary (string key and string value) entries.
+
+        Example of return value:
+        [
+            {
+                'age': '00:00:01.999',
+                'type': 'ICMP6(EchoReply)',
+                'len': '16',
+                'sec': 'yes',
+                'prio': 'norm',
+                'to': '0xac00',
+                'tx-success': 'yes',
+                'radio': '15.4',
+                'src': '[fd00:db8:0:0:ed7e:2d04:e543:eba5]:0',
+                'dst': '[fd00:db8:0:0:2cfa:fd61:58a9:f0aa]:0',
+
+            }
+        ]
+        """
+        cmd = f'history tx list {num_entries}'
+        self.send_command(cmd)
+        return self._parse_history_rx_tx_ouput(self._expect_command_output(cmd))
+
+    def _parse_history_rx_tx_ouput(self, lines):
+        rxtx_list = []
+        for line in lines:
+            if line.strip().startswith('type:'):
+                for item in line.strip().split(' '):
+                    k, v = item.split(':')
+                    entry[k] = v
+            elif line.strip().startswith('src:'):
+                entry['src'] = line[4:]
+            elif line.strip().startswith('dst:'):
+                entry['dst'] = line[4:]
+                rxtx_list.append(entry)
+            else:
+                entry = {}
+                entry['age'] = line
+
+        return rxtx_list
+
 
 class Node(NodeImpl, OtCli):
     pass
diff --git a/tests/scripts/thread-cert/test_history_tracker.py b/tests/scripts/thread-cert/test_history_tracker.py
new file mode 100755
index 0000000..6d2efee
--- /dev/null
+++ b/tests/scripts/thread-cert/test_history_tracker.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2021, The OpenThread Authors.
+#  All rights reserved.
+#
+#  Redistribution and use in source and binary forms, with or without
+#  modification, are permitted provided that the following conditions are met:
+#  1. Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+#  2. Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#  3. Neither the name of the copyright holder nor the
+#     names of its contributors may be used to endorse or promote products
+#     derived from this software without specific prior written permission.
+#
+#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+#  POSSIBILITY OF SUCH DAMAGE.
+#
+
+import os
+import unittest
+import sys
+import thread_cert
+
+# Test description:
+#   This test verifies History Tracker behavior.
+#
+# Topology:
+#
+#     LEADER
+#       |
+#       |
+#     CHILD
+#
+
+LEADER = 1
+CHILD = 2
+
+SHORT_WAIT = 5
+ONE_DAY = 24 * 60 * 60
+MAX_AGE_IN_DAYS = 49
+
+
+class TestHistoryTracker(thread_cert.TestCase):
+    USE_MESSAGE_FACTORY = False
+    SUPPORT_NCP = False
+
+    TOPOLOGY = {
+        LEADER: {
+            'name': 'Leader',
+            'mode': 'rdn',
+        },
+        CHILD: {
+            'name': 'Child',
+            'mode': 'n',
+        },
+    }
+
+    def test(self):
+        leader = self.nodes[LEADER]
+        child = self.nodes[CHILD]
+
+        # Start the leader and verify that 'netinfo' history
+        # is updated correctly.
+
+        leader.start()
+        self.simulator.go(SHORT_WAIT)
+        self.assertEqual(leader.get_state(), 'leader')
+
+        netinfo = leader.history_netinfo()
+        self.assertEqual(len(netinfo), 2)
+        self.assertEqual(netinfo[0]['role'], 'leader')
+        self.assertEqual(netinfo[0]['mode'], 'rdn')
+        self.assertEqual(int(netinfo[0]['rloc16'], 16), leader.get_addr16())
+        self.assertEqual(netinfo[0]['partition-id'], leader.get_partition_id())
+        self.assertEqual(netinfo[1]['role'], 'detached')
+
+        # Stop the leader
+
+        leader.thread_stop()
+        leader.interface_down()
+        self.simulator.go(SHORT_WAIT)
+        netinfo = leader.history_netinfo(2)
+        self.assertEqual(len(netinfo), 2)
+        self.assertEqual(netinfo[0]['role'], 'disabled')
+        self.assertEqual(netinfo[1]['role'], 'leader')
+
+        # Wait for one day, two days, then up to max age and verify that
+        # `netinfo` entry age is updated correctly.
+        #
+        # Since we want to wait for long duration (49 days), to speed up
+        # the simulation time, we disable leader to avoid the need to
+        # to simulate all the message/events (e.g. MLE adv) while thread
+        # is operational.
+
+        self.simulator.go(ONE_DAY)
+        netinfo = leader.history_netinfo(1)
+        self.assertTrue(netinfo[0]['age'].startswith('1 day'))
+
+        self.simulator.go(ONE_DAY)
+        netinfo = leader.history_netinfo(1)
+        self.assertTrue(netinfo[0]['age'].startswith('2 days'))
+
+        self.simulator.go((MAX_AGE_IN_DAYS - 3) * ONE_DAY)
+        netinfo = leader.history_netinfo(1)
+        self.assertTrue(netinfo[0]['age'].startswith('{} days'.format(MAX_AGE_IN_DAYS - 1)))
+
+        self.simulator.go(ONE_DAY)
+        netinfo = leader.history_netinfo(1)
+        self.assertTrue(netinfo[0]['age'].startswith('more than {} days'.format(MAX_AGE_IN_DAYS)))
+
+        self.simulator.go(2 * ONE_DAY)
+        netinfo = leader.history_netinfo(1)
+        self.assertTrue(netinfo[0]['age'].startswith('more than {} days'.format(MAX_AGE_IN_DAYS)))
+
+        # Start leader and child
+
+        leader.start()
+        self.simulator.go(SHORT_WAIT)
+        self.assertEqual(leader.get_state(), 'leader')
+
+        child.start()
+        self.simulator.go(SHORT_WAIT)
+        self.assertEqual(child.get_state(), 'child')
+
+        child_rloc16 = child.get_addr16()
+        leader_rloc16 = leader.get_addr16()
+
+        # Verify the `netinfo` history on child
+
+        netinfo = child.history_netinfo(2)
+        self.assertEqual(len(netinfo), 2)
+        self.assertEqual(netinfo[0]['role'], 'child')
+        self.assertEqual(netinfo[0]['mode'], 'n')
+        self.assertEqual(int(netinfo[0]['rloc16'], 16), child_rloc16)
+        self.assertEqual(netinfo[0]['partition-id'], leader.get_partition_id())
+        self.assertEqual(netinfo[1]['role'], 'detached')
+
+        # Change the child mode and verify that `netinfo` history
+        # records this change.
+
+        child.set_mode('rn')
+        self.simulator.go(SHORT_WAIT)
+        netinfo = child.history_netinfo(1)
+        self.assertEqual(len(netinfo), 1)
+        self.assertEqual(netinfo[0]['mode'], 'rn')
+
+        # Ping from leader to child and check the RX and TX history
+        # on child and leader.
+
+        child_mleid = child.get_mleid()
+        leader_mleid = leader.get_mleid()
+
+        ping_sizes = [10, 100, 1000]
+        num_msgs = len(ping_sizes)
+
+        for size in ping_sizes:
+            leader.ping(child_mleid, size=size)
+
+        leader_tx = leader.history_tx(num_msgs)
+        leader_rx = leader.history_rx(num_msgs)
+        child_tx = child.history_tx(num_msgs)
+        child_rx = child.history_rx(num_msgs)
+
+        for index in range(num_msgs):
+            self.assertEqual(leader_tx[index]['type'], 'ICMP6(EchoReqst)')
+            self.assertEqual(leader_tx[index]['sec'], 'yes')
+            self.assertEqual(leader_tx[index]['prio'], 'norm')
+            self.assertEqual(leader_tx[index]['tx-success'], 'yes')
+            self.assertEqual(leader_tx[index]['radio'], '15.4')
+            self.assertEqual(int(leader_tx[index]['to'], 16), child_rloc16)
+            self.assertEqual(leader_tx[index]['src'][1:-3], leader_mleid)
+            self.assertEqual(leader_tx[index]['dst'][1:-3], child_mleid)
+
+            self.assertEqual(child_rx[index]['type'], 'ICMP6(EchoReqst)')
+            self.assertEqual(child_rx[index]['sec'], 'yes')
+            self.assertEqual(child_rx[index]['prio'], 'norm')
+            self.assertEqual(child_rx[index]['radio'], '15.4')
+            self.assertEqual(int(child_rx[index]['from'], 16), leader_rloc16)
+            self.assertEqual(child_rx[index]['src'][1:-3], leader_mleid)
+            self.assertEqual(child_rx[index]['dst'][1:-3], child_mleid)
+
+            self.assertEqual(leader_rx[index]['type'], 'ICMP6(EchoReply)')
+            self.assertEqual(child_tx[index]['type'], 'ICMP6(EchoReply)')
+
+            self.assertEqual(leader_tx[index]['len'], child_rx[index]['len'])
+            self.assertEqual(leader_rx[index]['len'], child_tx[index]['len'])
+
+
+if __name__ == '__main__':
+    #  FIXME: We skip the test under distcheck build (the simulation
+    #  under this build for some reason cannot seem to handle longer
+    #  wait times - days up to 50 days in this test). We return error
+    #  code 77 which indicates that this test case was skipped (in
+    #  automake).
+
+    if os.getenv('DISTCHECK_BUILD') == '1':
+        sys.exit(77)
+
+    unittest.main()
diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h
index 9f1a4ce..00f5dd8 100644
--- a/tests/toranj/openthread-core-toranj-config.h
+++ b/tests/toranj/openthread-core-toranj-config.h
@@ -486,6 +486,14 @@
  */
 #define OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE 1
 
+/**
+ * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+ *
+ * Define as 1 to enable History Tracker module.
+ *
+ */
+#define OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE 1
+
 #if OPENTHREAD_RADIO
 /**
  * @def OPENTHREAD_CONFIG_MAC_SOFTWARE_ACK_TIMEOUT_ENABLE