In my efforts to continue building my personal web sites, I wanted to see where my visitors are coming from and how they are getting to my web site.  Unfortunately, my ISP doesn't provide me with any kind of stats or access to the logs, so I'm kind of screwed there.  So, I decided to take things into my own hands, and wrote a user control that has relatively little UI (and even then it's easy to disable). 

My solution uses an XML file in the app_data directory.  I chose this as it was pretty easy to test and write, but I found out when I went to deploy it, my ISP won't let me write to files in my app_data directory either.  The solution I ended up using was a SQL one, but for now, I'm going to post the XML file version.  There are some issues with this, it does lock the file while the data is updated, so you run the risk of slowing your website down.  I took this as an acceptable side effect on my website as I don't get a lot of hits, and if it takes an extra second or two to load, people can deal.

<%@ Control Language="C#" ClassName="ReferralTracker" %>
<%@ Import Namespace="System.Data" %>

<script runat="server">
    public bool ShowAtRuntime = false;
    
    protected void Page_Load(object sender, EventArgs e)
    {
	//The page that was requested
        string PageID = Request.RawUrl;
	//Where the user came from
        string Referrer = "";

	//If there is no referring page, Request.UrlReferrer is null
        if (Request.UrlReferrer != null)
            Referrer = Request.UrlReferrer.ToString();
        lblReferral.Text = "Referred By: " + Referrer;
        if (ShowAtRuntime)
            lblReferral.Visible = true;
        string XsdFile = Server.MapPath("~") + "\\App_Data\\referral.xsd";
        string XmlFile = Server.MapPath("~") + "\\App_Data\\referrals.xml";

        object fileLock = new object();

        System.Diagnostics.Debug.WriteLine("Referral: " + Referrer);

	//Lock the code so we don't have to worry about data loss
        lock (fileLock)
        {
            DataSet dsData = new DataSet();
            dsData.ReadXmlSchema(XsdFile);
            try
            {
                dsData.ReadXml(XmlFile);
            }
            catch (System.IO.FileNotFoundException ex)
            {
                System.Diagnostics.Debug.WriteLine("Creating file");
            }

            DataTable dt = dsData.Tables[0];
            string[] keys = new string[2];
            if (Referrer != null)
                keys[0] = Referrer.ToLower();
            else
                keys[0] = "";
            keys[1] = PageID;
            //See if this request has ever been hit before
            DataRow dr = dt.Rows.Find(keys);
            if (dr != null)
            {
                //It's been hit before, so lets increment the counter
                long iHits = (Int64)dr["Hits"];
                dr["Hits"] = ++iHits;
                dr["LastDateHit"] = DateTime.Now.ToString();
                dr.AcceptChanges();
                dt.AcceptChanges();
            }
            else
            {
                //This is a new hit, lets add the record
                dr = dt.NewRow();
                if (Referrer != null)
                    dr["Referrer"] = Referrer.ToLower();
                else
                    dr["Referrer"] = "";
                dr["Hits"] = 1;
                dr["Page"] = PageID;
                dr["FirstDateHit"] = DateTime.Now.ToString();
                dr["LastDateHit"] = DateTime.Now.ToString();
                dt.Rows.Add(dr);
                dt.AcceptChanges();
            }
            dsData.WriteXml(XmlFile);
        }
    }
</script>

<asp:Label ID="lblReferral" runat="server" Visible="false" />

I've put in a few comments so you can understand what I did.  It does depend on an XSD file to set things up, which is below:

<?xml version="1.0" standalone="yes"?>
<xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
  <xs:element name="NewDataSet" msdata:IsDataSet="true">
    <xs:complexType>
      <xs:choice maxOccurs="unbounded">
        <xs:element name="Referrals" msdata:CaseSensitive="False">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="Referrer" type="xs:string" />
              <xs:element name="ID" type="xs:string" />
              <xs:element name="Hits" type="xs:string" minOccurs="0" />
              <xs:element name="FirstDateHit" type="xs:string" minOccurs="0" />
              <xs:element name="LastDateHit" type="xs:string" minOccurs="0" />
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
    <xs:unique name="Constraint1" msdata:PrimaryKey="true">
      <xs:selector xpath=".//Referrals" />
      <xs:field xpath="Referrer" />
      <xs:field xpath="ID" />
    </xs:unique>
  </xs:element>
</xs:schema>

The solution I ended up using gets the data from an SQL database instead of an XML file, but it still does the same thing where it reads from the table to find out if the hit has happened before, so it can then increase the count, or if it's a fresh hit, it'll just add a new entry to the table.