Automating Azure DevOps Wiki Page Retrieval

If you’re managing documentation through Azure DevOps Wiki, keeping track of what’s stale and what’s active can be tricky—especially in large projects. In this blog post, we’ll walk through a handy C# script that retrieves detailed activity statistics on wiki pages including views, last update dates, and contributors.

Whether you’re a documentation owner, a tech lead, or just trying to clean up your team’s internal wiki, this solution will help you identify pages that might need some attention.


What Does the Script Do?

This C# script connects to Azure DevOps using REST APIs and performs the following:

  1. Authenticates using Personal Access Token (PAT)
  2. Fetches all wikis associated with a project
  3. Retrieves basic info about repositories
  4. Fetches batch wiki pages and handles continuation tokens
  5. Gathers view statistics for each page over the last 30 days
  6. Pulls last update metadata from Git history
  7. Generates an HTML report with sortable data

Key Components of the Code

Authentication

client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.ASCII.GetBytes($":{personalAccessToken}")));

The script uses basic auth with a PAT. Make sure to keep your token secure and don’t check it into source control.


Getting Wiki and Repo Details

var getWikisUrl = $"https://dev.azure.com/{org}/{project}/_apis/wiki/wikis";

It starts by listing all wikis in your project and then retrieves repository info like the default branch.


Fetching Wiki Pages

Using the pagesbatch endpoint, the script fetches wiki pages in batches and handles continuation tokens to manage paging:

string restApiUrl = $"https://dev.azure.com/{org}/{project}/_apis/wiki/wikis/{wiki.id}/pagesbatch?api-version=7.1";

Getting Page Views and Git History

The script gets:

  • View counts via the /stats endpoint
  • Last updated date and author via the Git dataProviders endpoint

This allows you to identify:

  • Stale pages (long time since last update)
  • Pages with high/low engagement

Output: HTML Report

Finally, it outputs an HTML table like this:

Wiki PageLast UpdatedUpdated ByDays Since UpdateViews (30d)
/Home2024-12-01John Doe125234

A quick glance lets you know which pages need love ✨


🛠️ How You Can Use It

  • Documentation Review: Identify pages that haven’t been touched in months
  • Cleanup Initiatives: Archive low-view, outdated pages
  • Engagement Analysis: See what content your team uses the most

🧪 Requirements

  • Azure DevOps PAT with access to Wiki and Git
  • Newtonsoft.Json (JsonConvert)
  • .NET Console App

⚠️ Be careful with your PAT. Use environment variables or secure storage solutions in real scenarios.

A word on security

✅ Summary

This script is a great way to automate your internal documentation audits. By combining view stats with update history, you gain insights into how your team interacts with your content—and where you can improve.

Azure DevOps Wiki Statistics Report C#

// AzureDevOps_WikiStats.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

class Program
{
    static string azureDevOpsUrl = "https://dev.azure.com/{org}";
    static string personalAccessToken = "YOUR_PERSONAL_ACCESS_TOKEN";
    static readonly string repoID = "{wiki_id}";
    static readonly string projectID = "{project_id}";
    static readonly string projectName = "{project_name}";

    static async Task Main(string[] args)
    {
        using var client = new HttpClient();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
            Convert.ToBase64String(Encoding.ASCII.GetBytes($":{personalAccessToken}")));

        string getWikisUrl = $"{azureDevOpsUrl}/{projectName}/_apis/wiki/wikis";
        var wikiResponse = await client.GetAsync(getWikisUrl);
        wikiResponse.EnsureSuccessStatusCode();
        var wikiList = JsonConvert.DeserializeObject<JObject>(await wikiResponse.Content.ReadAsStringAsync())["value"];

        foreach (var wiki in wikiList)
        {
            string getRepoUrl = $"{azureDevOpsUrl}/{projectName}/_apis/git/repositories/{repoID}?api-version=7.1";
            var repoResponse = await client.GetAsync(getRepoUrl);
            repoResponse.EnsureSuccessStatusCode();

            string defaultBranch = JObject.Parse(await repoResponse.Content.ReadAsStringAsync())["defaultBranch"]?.ToString()?.Replace("refs/heads/", "");
            Console.WriteLine($"Repo: {wiki["name"]}, Default Branch: {defaultBranch}");

            List<(string Title, string Link, string LastUpdated, string UpdatedBy, int DaysAgo, int Views)> wikiPageData = new();

            string restApiUrl = $"{azureDevOpsUrl}/{projectName}/_apis/wiki/wikis/{wiki["id"]}/pagesbatch?api-version=7.1";
            string obj = JsonConvert.SerializeObject(new { top = 25, pageViewsForDays = 30 });
            var content = new StringContent(obj, Encoding.UTF8, "application/json");

            var pagesResponse = await client.PostAsync(restApiUrl, content);
            pagesResponse.EnsureSuccessStatusCode();
            var pages = JObject.Parse(await pagesResponse.Content.ReadAsStringAsync())["value"];

            foreach (var page in pages)
            {
                string pageId = page["id"].ToString();
                string pagePath = page["path"].ToString();
                string pageLink = $"{azureDevOpsUrl}/{projectName}/_wiki/wikis/{wiki["name"]}/{pageId}";
                string statsUrl = $"{azureDevOpsUrl}/{projectName}/_apis/wiki/wikis/{wiki["id"]}/pages/{pageId}/stats?pageViewsForDays=30";
                int views = 0;

                var statsResponse = await client.GetAsync(statsUrl);
                if (statsResponse.IsSuccessStatusCode)
                {
                    var stats = JObject.Parse(await statsResponse.Content.ReadAsStringAsync())["viewStats"];
                    views = stats?.Sum(s => (int)s["count"]) ?? 0;
                }

                string commitPath = Uri.EscapeDataString(pagePath + ".md");
                string jsonPayload = JsonConvert.SerializeObject(new
                {
                    context = new
                    {
                        properties = new
                        {
                            repositoryId = wiki["id"].ToString(),
                            searchCriteria = new
                            {
                                gitCommitLookupArguments = (object)null,
                                gitHistoryQueryArguments = new
                                {
                                    startFromVersion = new { versionOptions = 0, versionType = 0, version = "wikiMaster" },
                                    order = 0,
                                    path = commitPath,
                                    historyMode = 0,
                                    stopAtAdds = false,
                                    author = (object)null,
                                    committer = (object)null
                                },
                                gitArtifactsQueryArguments = (object)null,
                                gitGraphQueryArguments = new { fetchGraph = false, emptyLineLengthLimit = 20, emptyLineLengthMultiplier = false, order = 0 }
                            }
                        }
                    },
                    contributionIds = new[] { "ms.vss-code-web.git-history-view-commits-data-provider" }
                });

                var revContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
                var revResponse = await client.PostAsync($"{azureDevOpsUrl}/_apis/Contribution/dataProviders/query/project/{projectID}?api-version=5.1-preview.1", revContent);

                string lastUpdated = "", updatedBy = "";
                int daysAgo = -1;

                if (revResponse.IsSuccessStatusCode)
                {
                    var commitData = JObject.Parse(await revResponse.Content.ReadAsStringAsync());
                    var commits = commitData["data"]["msvsscodewebgithistoryviewcommitsdataprovider"]["commits"];
                    var firstCommit = commits?.FirstOrDefault();
                    if (firstCommit != null)
                    {
                        DateTime commitDate = DateTime.Parse(firstCommit["author"]["date"].ToString());
                        lastUpdated = commitDate.ToString("yyyy-MM-dd");
                        updatedBy = firstCommit["author"]["name"].ToString();
                        daysAgo = (DateTime.UtcNow - commitDate).Days;
                    }
                }

                wikiPageData.Add((pagePath, pageLink, lastUpdated, updatedBy, daysAgo, views));
            }

            var sortedData = wikiPageData.OrderByDescending(x => x.DaysAgo);
            var sb = new StringBuilder();
            sb.AppendLine("<html><head><title>Wiki Report</title></head><body>");
            sb.AppendLine($"<h1>Wiki Activity Report: {wiki["name"]}</h1><table border='1'><tr><th>Title</th><th>Link</th><th>Last Updated</th><th>Updated By</th><th>Days Ago</th><th>Views (30d)</th></tr>");

            foreach (var entry in sortedData)
            {
                sb.AppendLine($"<tr><td>{entry.Title}</td><td><a href='{entry.Link}'>Link</a></td><td>{entry.LastUpdated}</td><td>{entry.UpdatedBy}</td><td>{entry.DaysAgo}</td><td>{entry.Views}</td></tr>");
            }

            sb.AppendLine("</table></body></html>");

            File.WriteAllText("WikiReport.html", sb.ToString());
            Console.WriteLine("Wiki report generated: WikiReport.html");
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.