diff --git a/scrapy_fieldstats/fieldstats.py b/scrapy_fieldstats/fieldstats.py
index 703b2dc..83d9860 100644
--- a/scrapy_fieldstats/fieldstats.py
+++ b/scrapy_fieldstats/fieldstats.py
@@ -1,7 +1,6 @@
 # -*- coding:utf-8 -*-
 import logging
 import pprint
-from collections import defaultdict
 
 from scrapy import signals
 from scrapy.exceptions import NotConfigured
@@ -10,12 +9,12 @@ logger = logging.getLogger(__name__)
 
 
 class FieldStatsExtension(object):
-    """ When enabled, the FieldStats extensions logs the percentage of
+    """ When enabled, the FieldStats extension logs the percentage of
         items coverage for a crawl.
     """
     def __init__(self):
         self.item_count = 0
-        self.field_counts = defaultdict(int)
+        self.field_counts = {}
 
     @classmethod
     def from_crawler(cls, crawler):
@@ -28,20 +27,46 @@ class FieldStatsExtension(object):
         return ext
 
     def item_scraped(self, item, spider):
+        self.compute_item(item)
+
+    def spider_closed(self, spider):
+        fields_summary = self.build_fields_summary()
+        logger.info('Field stats:\n{}'.format(pprint.pformat(fields_summary)))
+
+    def compute_item(self, item):
         self.item_count += 1
+        self.count_item_fields(item)
+
+    def count_item_fields(self, item, current_node=None):
+        if current_node is None:
+            current_node = self.field_counts
+
         for name, value in item.items():
             if not value:
                 continue
-            self.field_counts[name] += 1
 
-    def spider_closed(self, spider):
-        field_stats = self.compute_fieldstats()
-        logger.info('Field stats:\n{}'.format(pprint.pformat(field_stats)))
+            if isinstance(value, dict):
+                # recurse into nested items
+                if name not in current_node:
+                    current_node[name] = {}
+                self.count_item_fields(value, current_node=current_node[name])
+                continue
+
+            if name not in current_node:
+                current_node[name] = 0
+            current_node[name] += 1
+
+    def build_fields_summary(self, field_counts=None, fields_summary=None):
+        if field_counts is None:
+            field_counts = self.field_counts
+            fields_summary = {}
 
-    def compute_fieldstats(self):
-        field_stats = {}
-        for name, count in self.field_counts.items():
-            field_coverage = int(count) * 100 / self.item_count
-            field_stats[name] = "{}%".format(field_coverage)
+        for name, value in field_counts.items():
+            if isinstance(value, dict):
+                fields_summary[name] = {}
+                self.build_fields_summary(field_counts[name], fields_summary[name])
+            else:
+                field_percentage = int(value) * 100 / self.item_count
+                fields_summary[name] = "{}%".format(field_percentage)
 
-        return field_stats
+        return fields_summary

Support nested items
Currently, the extension only counts shallow items in dictionaries.

However, it's quite common to have nested items such as:

```json
{
    "title": "Animal Farm",
    "author": {
        "name": "George Orwell",
        "birth_location": "Motihari, India",
        "birth_date": "1903-06-25"
    },
    "ratings": 4.9
}
```

In this case, the extension doesn't generate coverage stats for the nested `name`, `birth_location` and `birth_date` fields.

diff --git a/tests/test_scrapy_fieldstats.py b/tests/test_scrapy_fieldstats.py
index 2873777..24b749f 100644
--- a/tests/test_scrapy_fieldstats.py
+++ b/tests/test_scrapy_fieldstats.py
@@ -3,18 +3,20 @@
 from scrapy_fieldstats.fieldstats import FieldStatsExtension
 
 
-def fake_extract_items(fake_items, extension):
+def extract_fake_items_and_compute_stats(fake_items):
+    ext = FieldStatsExtension()
     for item in fake_items:
-        extension.item_scraped(item, None)
+        ext.compute_item(item)
+    field_stats = ext.build_fields_summary()
+    return field_stats
 
 
 def test_single_item():
     fake_items = [{"field1": "value1"}]
-    ext = FieldStatsExtension()
-    fake_extract_items(fake_items, ext)
-    field_stats = ext.compute_fieldstats()
+    field_stats = extract_fake_items_and_compute_stats(fake_items)
+
     assert len(field_stats) == 1
-    assert field_stats.get('field1') == '100.0%'
+    assert field_stats['field1'] == '100.0%'
 
 
 def test_single_item_many_fields():
@@ -24,21 +26,19 @@ def test_single_item_many_fields():
             "field2": "value2",
         }
     ]
-    ext = FieldStatsExtension()
-    fake_extract_items(fake_items, ext)
-    field_stats = ext.compute_fieldstats()
+    field_stats = extract_fake_items_and_compute_stats(fake_items)
+
     assert len(field_stats) == 2
-    assert field_stats.get('field1') == '100.0%'
-    assert field_stats.get('field2') == '100.0%'
+    assert field_stats['field1'] == '100.0%'
+    assert field_stats['field2'] == '100.0%'
 
 
 def test_many_items():
     fake_items = [{"field1": "value1"}, {"field1": "value1"}]
-    ext = FieldStatsExtension()
-    fake_extract_items(fake_items, ext)
-    field_stats = ext.compute_fieldstats()
+    field_stats = extract_fake_items_and_compute_stats(fake_items)
+
     assert len(field_stats) == 1
-    assert field_stats.get('field1') == '100.0%'
+    assert field_stats['field1'] == '100.0%'
 
 
 def test_many_items_many_fields():
@@ -52,12 +52,11 @@ def test_many_items_many_fields():
             "field2": "value2",
         }
     ]
-    ext = FieldStatsExtension()
-    fake_extract_items(fake_items, ext)
-    field_stats = ext.compute_fieldstats()
+    field_stats = extract_fake_items_and_compute_stats(fake_items)
+
     assert len(field_stats) == 2
-    assert field_stats.get('field1') == '100.0%'
-    assert field_stats.get('field2') == '100.0%'
+    assert field_stats['field1'] == '100.0%'
+    assert field_stats['field2'] == '100.0%'
 
 
 def test_many_items_many_fields_missing_field():
@@ -70,12 +69,11 @@ def test_many_items_many_fields_missing_field():
             "field2": "value2",
         }
     ]
-    ext = FieldStatsExtension()
-    fake_extract_items(fake_items, ext)
-    field_stats = ext.compute_fieldstats()
+    field_stats = extract_fake_items_and_compute_stats(fake_items)
+
     assert len(field_stats) == 2
-    assert field_stats.get('field1') == '100.0%'
-    assert field_stats.get('field2') == '50.0%'
+    assert field_stats['field1'] == '100.0%'
+    assert field_stats['field2'] == '50.0%'
 
 
 def test_many_items_many_fields_empty_field():
@@ -89,9 +87,41 @@ def test_many_items_many_fields_empty_field():
             "field2": "value2",
         }
     ]
-    ext = FieldStatsExtension()
-    fake_extract_items(fake_items, ext)
-    field_stats = ext.compute_fieldstats()
+    field_stats = extract_fake_items_and_compute_stats(fake_items)
+
     assert len(field_stats) == 2
-    assert field_stats.get('field1') == '100.0%'
-    assert field_stats.get('field2') == '50.0%'
+    assert field_stats['field1'] == '100.0%'
+    assert field_stats['field2'] == '50.0%'
+
+
+def test_nested_items():
+    fake_items = [
+        {
+            "field1": "value1",
+            "field2": {
+                "field2.1": "value2.1",
+                "field2.2": "value2.2",
+                "field2.3": {
+                    "field2.3.1": "value2.3.1",
+                    "field2.3.2": "value2.3.2",
+                },
+            }
+        },
+        {
+            "field1": "value1",
+            "field2": {
+                "field2.1": "value2.1",
+                "field2.3": {
+                    "field2.3.1": "value2.3.1",
+                    "field2.3.2": "",
+                },
+                "field2.4": "value2.2",
+            }
+        }
+    ]
+    field_stats = extract_fake_items_and_compute_stats(fake_items)
+    assert field_stats['field1'] == '100.0%'
+    assert field_stats['field2']['field2.1'] == '100.0%'
+    assert field_stats['field2']['field2.2'] == '50.0%'
+    assert field_stats['field2']['field2.2'] == '50.0%'
+    assert field_stats['field2']['field2.3']['field2.3.2'] == '50.0%'

pip install -e .
parse_log_pytest
scrapy
pytest
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
appdirs==1.4.4
attrs @ file:///croot/attrs_1734533101012/work
Automat @ file:///tmp/build/80754af9/automat_1600298431173/work
bcrypt @ file:///croot/bcrypt_1736182451882/work
Brotli @ file:///croot/brotli-split_1736182456865/work
certifi @ file:///croot/certifi_1738623731865/work/certifi
cffi @ file:///croot/cffi_1736182485317/work
charset-normalizer @ file:///croot/charset-normalizer_1721748349566/work
constantly @ file:///croot/constantly_1703165600746/work
cryptography @ file:///croot/cryptography_1740577825284/work
cssselect @ file:///croot/cssselect_1707339882883/work
defusedxml @ file:///tmp/build/80754af9/defusedxml_1615228127516/work
exceptiongroup==1.2.2
filelock @ file:///croot/filelock_1700591183607/work
hyperlink @ file:///tmp/build/80754af9/hyperlink_1610130746837/work
idna @ file:///croot/idna_1714398848350/work
incremental @ file:///croot/incremental_1708639938299/work
iniconfig==2.1.0
itemadapter @ file:///tmp/build/80754af9/itemadapter_1626442940632/work
itemloaders @ file:///croot/itemloaders_1708639918324/work
jmespath @ file:///croot/jmespath_1700144569655/work
lxml @ file:///croot/lxml_1737039601731/work
packaging @ file:///croot/packaging_1734472117206/work
parsel @ file:///croot/parsel_1707503445438/work
pluggy==1.5.0
Protego @ file:///tmp/build/80754af9/protego_1598657180827/work
pyasn1 @ file:///croot/pyasn1_1729239786406/work
pyasn1_modules @ file:///home/conda/feedstock_root/build_artifacts/pyasn1-modules_1733324602540/work
pycparser @ file:///tmp/build/80754af9/pycparser_1636541352034/work
PyDispatcher==2.0.5
pyOpenSSL @ file:///croot/pyopenssl_1741343803032/work
PySocks @ file:///tmp/build/80754af9/pysocks_1605305812635/work
pytest==8.3.5
queuelib @ file:///croot/queuelib_1696950067631/work
requests @ file:///croot/requests_1730999120400/work
requests-file @ file:///Users/ktietz/demo/mc3/conda-bld/requests-file_1629455781986/work
Scrapy @ file:///croot/scrapy_1733166797775/work
-e git+https://github.com/stummjr/scrapy-fieldstats.git@476b8a2bdb01ab3f77a6dc5af936284f41a20272#egg=scrapy_fieldstats
service-identity @ file:///Users/ktietz/demo/mc3/conda-bld/service_identity_1629460757137/work
six @ file:///tmp/build/80754af9/six_1644875935023/work
tldextract @ file:///croot/tldextract_1723064386918/work
tomli==2.2.1
Twisted @ file:///croot/twisted_1708702809815/work
typing_extensions @ file:///croot/typing_extensions_1734714854207/work
urllib3 @ file:///croot/urllib3_1737133630106/work
w3lib @ file:///croot/w3lib_1708639924738/work
zope.interface @ file:///croot/zope.interface_1731939362051/work

name: scrapy-fieldstats
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - appdirs=1.4.4=pyhd3eb1b0_0
  - attrs=24.3.0=py39h06a4308_0
  - automat=20.2.0=py_0
  - bcrypt=3.2.0=py39h5eee18b_2
  - brotli-python=1.0.9=py39h6a678d5_9
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2025.1.31=py39h06a4308_0
  - cffi=1.17.1=py39h1fdaa30_1
  - charset-normalizer=3.3.2=pyhd3eb1b0_0
  - constantly=23.10.4=py39h06a4308_0
  - cryptography=44.0.1=py39h7825ff9_0
  - cssselect=1.2.0=py39h06a4308_0
  - defusedxml=0.7.1=pyhd3eb1b0_0
  - filelock=3.13.1=py39h06a4308_0
  - hyperlink=21.0.0=pyhd3eb1b0_0
  - icu=73.1=h6a678d5_0
  - idna=3.7=py39h06a4308_0
  - incremental=22.10.0=pyhd3eb1b0_0
  - itemadapter=0.3.0=pyhd3eb1b0_0
  - itemloaders=1.1.0=py39h06a4308_0
  - jmespath=1.0.1=py39h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - libxml2=2.13.5=hfdd30dd_0
  - libxslt=1.1.41=h097e994_0
  - lxml=5.3.0=py39h57af460_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - parsel=1.8.1=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - protego=0.1.16=py_0
  - pyasn1=0.6.1=py39h06a4308_0
  - pyasn1-modules=0.4.1=pyhd8ed1ab_1
  - pycparser=2.21=pyhd3eb1b0_0
  - pydispatcher=2.0.5=py39h06a4308_2
  - pyopenssl=25.0.0=py39h06a4308_0
  - pysocks=1.7.1=py39h06a4308_0
  - python=3.9.21=he870216_1
  - queuelib=1.6.2=py39h06a4308_0
  - readline=8.2=h5eee18b_0
  - requests=2.32.3=py39h06a4308_1
  - requests-file=1.5.1=pyhd3eb1b0_0
  - scrapy=2.12.0=py39h06a4308_0
  - service_identity=18.1.0=pyhd3eb1b0_1
  - setuptools=75.8.0=py39h06a4308_0
  - six=1.16.0=pyhd3eb1b0_1
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tldextract=5.1.2=py39h06a4308_0
  - twisted=23.10.0=py39h06a4308_0
  - typing_extensions=4.12.2=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - urllib3=2.3.0=py39h06a4308_0
  - w3lib=2.1.2=py39h06a4308_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - zope=1.0=py39h06a4308_1
  - zope.interface=7.1.1=py39h5eee18b_0
  - pip:
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - pluggy==1.5.0
      - pytest==8.3.5
      - tomli==2.2.1
prefix: /opt/conda/envs/scrapy-fieldstats

tests/test_scrapy_fieldstats.py::test_single_item tests/test_scrapy_fieldstats.py::test_single_item_many_fields tests/test_scrapy_fieldstats.py::test_many_items tests/test_scrapy_fieldstats.py::test_many_items_many_fields tests/test_scrapy_fieldstats.py::test_many_items_many_fields_missing_field tests/test_scrapy_fieldstats.py::test_many_items_many_fields_empty_field tests/test_scrapy_fieldstats.py::test_nested_items
scrapy_fieldstats/fieldstats.py
scrapy_fieldstats/fieldstats.py