def METHOD_NAME(projects: Sequence[Project]) -> None:
    num_clustered = 0
    try:
        for project in projects:
            with sentry_sdk.start_span(op="span_descs-cluster") as span:
                span.set_data("project_id", project.id)
                descriptions = list(redis.get_span_descriptions(project))
                new_rules = []
                if len(descriptions) >= MERGE_THRESHOLD:
                    clusterer = TreeClusterer(merge_threshold=MERGE_THRESHOLD)
                    clusterer.add_input(descriptions)
                    new_rules = clusterer.get_rules()
                    # Span description rules must match a prefix in the string
                    # (HTTP verb, domain...), but we only feed the URL path to
                    # the clusterer to avoid scrubbing other tokens. The prefix
                    # `**` in the glob ensures we match the prefix but we don't
                    # scrub it.
                    new_rules = [ReplacementRule(f"**{r}") for r in new_rules]
                track_clusterer_run(ClustererNamespace.SPANS, project)
                # The Redis store may have more up-to-date last_seen values,
                # so we must update the stores to bring these values to
                # project options, even if there aren't any new rules.
                num_rules_added = rules.update_rules(ClustererNamespace.SPANS, project, new_rules)
                # Track a global counter of new rules:
                metrics.incr("span_descs.new_rules_discovered", num_rules_added, sample_rate=1.0)
                # Clear transaction names to prevent the set from picking up
                # noise over a long time range.
                redis.clear_samples(ClustererNamespace.SPANS, project)
            num_clustered += 1
    finally:
        unclustered = len(projects) - num_clustered
        if unclustered > 0:
            metrics.incr(
                "span_descs.cluster_projects.unclustered", amount=unclustered, sample_rate=1.0
            )