Permalink
Browse files

Initial 'working' version

  • Loading branch information...
1 parent 64720d5 commit 0f8b2337f5f6b78b00a4ff1570c1e4533622fb4d @vidarh committed Feb 14, 2009
File renamed without changes.
View
@@ -0,0 +1,94 @@
+# Rakefile for SlidingStats. -*-ruby-*-
+# Shamelessly stolen from Rack::Contrib
+
+require 'rake/rdoctask'
+require 'rake/testtask'
+
+desc "Run all the tests"
+#task :default => [:test]
+
+#desc "Generate RDox"
+#task "RDOX" do
+# sh "specrb -Ilib:test -a --rdox >RDOX"
+#end
+
+#desc "Run all the fast tests"
+#task :test do
+# sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}"
+#end
+
+#desc "Run all the tests"
+#task :fulltest do
+# sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}"
+#end
+
+desc "Generate RDoc documentation"
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.options << '--line-numbers' << '--inline-source' <<
+ '--main' << 'README' <<
+ '--title' << "Sliding Stats Documentation" <<
+ '--charset' << 'utf-8'
+ rdoc.rdoc_dir = "doc"
+ rdoc.rdoc_files.include 'README.rdoc'
+ rdoc.rdoc_files.include 'RDOX'
+ rdoc.rdoc_files.include("lib/sliding-stats/*.rb")
+ rdoc.rdoc_files.include("lib/sliding-stats/*/*.rb")
+end
+task :rdoc => ["RDOX"]
+
+
+# PACKAGING =================================================================
+
+# load gemspec like github's gem builder to surface any SAFE issues.
+require 'rubygems/specification'
+$spec = eval(File.read("sliding-stats.gemspec"))
+
+def package(ext='')
+ "pkg/sliding-stats-#{$spec.version}" + ext
+end
+
+desc 'Build packages'
+task :package => %w[.gem .tar.gz].map {|e| package(e)}
+
+desc 'Build and install as local gem'
+task :install => package('.gem') do
+ sh "gem install #{package('.gem')}"
+end
+
+directory 'pkg/'
+
+file package('.gem') => %w[pkg/ sliding-stats.gemspec] + $spec.files do |f|
+ sh "gem build sliding-stats.gemspec"
+ mv File.basename(f.name), f.name
+end
+
+file package('.tar.gz') => %w[pkg/] + $spec.files do |f|
+ sh "git archive --format=tar HEAD | gzip > #{f.name}"
+end
+
+# desc 'Publish gem and tarball to rubyforge'
+# task 'publish:gem' => [package('.gem'), package('.tar.gz')] do |t|
+# sh < <-end
+# rubyforge add_release rack rack-contrib #{$spec.version} #{package('.gem')} &&
+# rubyforge add_file rack rack-contrib #{$spec.version} #{package('.tar.gz')}
+# end
+# end
+
+# GEMSPEC ===================================================================
+
+file 'sliding-stats.gemspec' => FileList['{lib,test}/**','Rakefile', 'README.rdoc'] do |f|
+ # read spec file and split out manifest section
+ spec = File.read(f.name)
+ parts = spec.split(" # = MANIFEST =\n")
+ fail 'bad spec' if parts.length != 3
+ # determine file list from git ls-files
+ files = `git ls-files`.
+ split("\n").sort.reject{ |file| file =~ /^\./ }.
+ map{ |file| " #{file}" }.join("\n")
+ # piece file back together and write...
+ parts[1] = " s.files = %w[\n#{files}\n ]\n"
+ spec = parts.join(" # = MANIFEST =\n")
+ spec.sub!(/s.date = '.*'/, "s.date = '#{Time.now.strftime("%Y-%m-%d")}'")
+ File.open(f.name, 'w') { |io| io.write(spec) }
+ puts "updated #{f.name}"
+end
View
@@ -0,0 +1,42 @@
+
+# This demonstrates how to configure the stats and generates an SVG of 1000 requests by page.
+# The exclusion patterns are geared towards my website, and so you'd want to adapt them.
+
+require 'sliding-stats'
+
+opts = {
+ :limit => 1000,
+ :persist => 10,
+ :exclude_referers => [
+ /http:\/\/www\.hokstad\.com/, # Not interested in seeing internal clicks
+ /http:\/\/search.live.com\/results.aspx/, # MSN referer spam
+ /^-/
+ ],
+ :exclude_pages => [
+ /\/referers/, /\/stats.*/,
+ /\.xml/, /\/feed/, /\.rdf/, /\.ico/, /\/static\//,/\/robots.txt/
+ ],
+ :rewrite_referers =>
+ [
+ [/http:\/\/.*\.google\..*?[?&]q=([^&]*)?&*.*/,"Google Search: '\\1'"],
+ [/http:\/\/www.google..*\/reader.*/,"Google Reader"]
+ ]
+}
+
+view = SlidingStats::View.new(nil,"/stats")
+window = SlidingStats::Window.new(view, opts)
+
+# First we feed it stats from STDIN:
+
+STDIN.each do |line|
+ line = line.split(" ")
+ window.call({"REQUEST_URI" => line[6],
+ "HTTP_REFERER" => line[10][1..-2]})
+end
+
+# Then we fake a stats request:
+
+window.call({"REQUEST_URI" => "/stats/pages.svg"}).each do |line|
+ puts line
+end
+
@@ -0,0 +1,5 @@
+
+require 'sliding-stats/stats'
+require 'sliding-stats/window'
+require 'sliding-stats/view'
+require 'sliding-stats/persist'
@@ -0,0 +1,33 @@
+
+
+module SlidingStats
+
+ # This class provides basic persistence for SlidingStats
+ # To use it, simply add add :persist => [number of requests
+ # between saves] to the SlidingStats::Window options,
+ # or pass a different persistence class.
+ class Persist
+ def initialize every = 10,path="/var/tmp/slidingstats"
+ @every = every
+ @num = 0
+ @path = path
+ end
+
+ def load
+ begin
+ Marshal.load(File.read(@path))
+ rescue
+ []
+ end
+ end
+
+ def save requests
+ @num += 1
+ if (@num % @every) == 0
+ File.open(@path,"w") do |f|
+ f.write(Marshal.dump(requests))
+ end
+ end
+ end
+ end
+end
@@ -0,0 +1,66 @@
+
+module SlidingStats
+
+ # Calculates and maintains stats for a set of
+ # requests.
+ class Stats
+ attr_reader :referers, :pages, :referers_to_pages
+ def initialize request,ex_referers,ex_pages
+ @exclude_referers = ex_referers
+ @exclude_pages = ex_pages
+
+ @referers = {}
+ @pages = {}
+ @referers_to_pages = {} # Two level
+
+ request.each { |r| self.add(r) }
+ end
+
+ # Add a single line of stats data
+ def add r
+ ref = r["HTTP_REFERER"]
+ req = r["REQUEST_URI"]
+
+ ex_ref = @exclude_referers.detect{|pat| ref =~ pat}
+ ex_req = @exclude_pages.detect{|pat| req =~ pat}
+
+ if !ex_ref
+ @referers[ref] ||= 0
+ @referers[ref] += 1
+ end
+
+ if !ex_req
+ @pages[req] ||= 0
+ @pages[req] += 1
+ end
+
+ if !ex_ref && !ex_req
+ @referers_to_pages[ref] ||= {:total => 0}
+ @referers_to_pages[ref][req] ||= 0
+ @referers_to_pages[ref][req] += 1
+ @referers_to_pages[ref][:total] += 1
+ end
+ end
+
+ def sub r
+ ref = r["HTTP_REFERER"]
+ req = r["REQUEST_URI"]
+
+ ex_ref = @exclude_referer.detect{|pat| ref =~ pat}
+ ex_req = @exclude_referer.detect{|pat| req =~ pat}
+
+ if !ex_ref
+ @referers[ref] -= 1
+ end
+
+ if !ex_req
+ @pages[req] -= 1
+ end
+
+ if !ex_ref && !ex_req
+ @referers_to_pages[ref][req] -= 1
+ @referers_to_pages[ref][:total] -= 1
+ end
+ end
+ end
+end
@@ -0,0 +1,93 @@
+require 'SVG/Graph/BarHorizontal'
+require 'rack'
+
+module SlidingStats
+
+ # Provides a basic view of the stats. You can easily provide a custom
+ # view by subclassing and overriding the #show method, or replacing it
+ # completely.
+ class View
+ def initialize app, base
+ @app = app
+ @base = base
+ end
+
+ def show
+ r = Rack::Response.new
+ r.write("<html><head><title>Sliding Stats</title><style>h2 {margin-top: 20px;}</style> <body>")
+ r.write("<h1>Sliding Stats<h1>")
+ # Setting the size here is a *hack*. Need to fix that
+ r.write("<h2>Most recent referrers</h2>")
+ r.write("<div style='width: 1000px;'><embed pluginspage=\"http://www.adobe.com/svg/viewer/install/\" type=\"image/svg+xml\" src=\"#{@base}/referers.svg\" style=\"margin-left: 50px; width: 1000px; height: #{40 + 20*@window.stats.referers.size}px;\"></div>")
+ r.write("<h2>Most recent pages</h2>")
+ r.write("<div style='width: 1000px;'><embed pluginspage=\"http://www.adobe.com/svg/viewer/install/\" type=\"image/svg+xml\" src=\"#{@base}/pages.svg\" style=\"margin-left: 50px; width: 1000px; height: #{40 + 20*@window.stats.pages.size}px;\"></div>")
+ r.write("<h2>Most recent pages grouped by referrer</h2>")
+ r.write("<table border='1' style=\"display:inline; margin-top: 20px; margin-left: 100px; width: 950px; \"><tr><th>Referer</th><th>Pages</th></tr>\n")
+ @window.stats.referers_to_pages.sort_by{|k,v| -v[:total]}.each do |k,v|
+ r.write("<tr><td>#{CGI.escapeHTML(k)}</td> <td><table>")
+ total = v[:total]
+ if v.size > 2 # include :total
+ r.write("<tr><td>#{total}</td><td><strong>total</strong></td></tr>")
+ end
+ v.sort_by{|page,count| -count}.each do |page,count|
+ r.write("<tr><td>#{count}</td><td>#{page.to_s}</td></tr>") if page != :total
+ end
+ r.write("</table></td></tr>\n")
+ end
+ r.write("</table>")
+ r.write("<div style='margin-top: 50px'>Stats by <a href='http://www.hokstad.com/slidingstats'>Sliding Stats</a> -- Copyright 2009 <a href='http://www.hokstad.com/'>Vidar Hokstad</a></div>")
+ r.write("</body></html>")
+ r.finish
+ end
+
+ def show_svg(src)
+ fields = []
+ data = []
+ src.sort_by{|k,v| -v}.each do |k,v|
+ if k != "-" # Excluding because of referers
+ k = k[0..79] + "..." if k.length > 80
+ fields << CGI.escapeHTML(k)
+ data << v
+ end
+ end
+
+ if fields.empty?
+ r = Rack::Response.new("No data")
+ return r.finish
+ end
+
+ graph = SVG::Graph::BarHorizontal.new(
+ :height => 40 + 20 * data.size,
+ :width => 1000,
+ :fields => fields.reverse
+ )
+ graph.add_data(:data => data.reverse)
+ graph.rotate_y_labels = false
+ graph.scale_integers = true
+ graph.key = false
+ r = Rack::Response.new
+ r["Content-Type"] = "image/svg+xml"
+ r.write(graph.burn)
+ r.finish
+ end
+
+ def call env
+ return Rack::Response.new("Missing 'slidingstats' object -- did you forget to set up SlidingStats::Window before SlidingStats::View ? ").finish if !env["slidingstats"]
+
+ uri = env["REQUEST_URI"]
+ @window = env["slidingstats"]
+
+ case uri
+ when @base
+ return show
+ when @base+"/referers.svg"
+ return show_svg(@window.stats.referers)
+ when @base+"/pages.svg"
+ return show_svg(@window.stats.pages)
+ else
+ return @app.call(env) if @app
+ return Rack::Response.new("(empty)").finish
+ end
+ end
+ end
+end
Oops, something went wrong. Retry.

0 comments on commit 0f8b233

Please sign in to comment.