One-Click Speedup für Apache-VHosts 0

Posted by fwoeck
on Thursday, July 23

Mit diesen allgemeinen Apache2-Optionen lässt sich relativ schnell und schmerzlos eine Verbesserung der Ladezeiten erreichen: Kompression und Expiration-Headers.

Die beiden verantwortlichen Module müssen aktiviert werden:

a2enmod expires
a2enmod deflate

und die globale Sektion der apache2.conf erweitert:

# gzip html, css and js
AddOutputFilterByType DEFLATE text/html text/css application/x-javascript application/javascript

# far future expires headers
<FilesMatch ".(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)?d{10}$">
  ExpiresDefault "access plus 10 years" 
</FilesMatch>

Zu beachten wäre, dass die statischen Dateien aufgrund der langen Lebensdauer nicht aus dem Cache der Browser verschwinden werden – es sei denn, man zwingt sie dazu!

Weblinks

speed-up-your-apachepassenger-rails-app-in-2min

Fake-a-web

Posted by fwoeck
on Saturday, June 13

Das fakeweb gem …

gem install fakeweb

... ist quasi für’s Web, was Mocks und Stubs für die Modelle – in Spec-Läufen kann man fakewebs für einzelne URLs definieren. Bei Net::HTTP-Aufrufen wird dann anstelle der richtigen Netzverbindung der vorher definierte lokale Inhalt ausgegeben.

Ich benutze Factory-Girl als Fixtureersatz und habe die Webdefinitionen fauler Weise mit in deren Datei spec/factories.rb gegeben, weil ich das irgendwie passend fand:

1
2
3
4
5
6
7
8
9
10
11
12
page1 = open("./spec/www_google_de.txt") {|f| f.read }
page2 = open("./spec/www_google_de_en.txt") {|f| f.read }

FakeWeb.register_uri(:get, "http://www.google.de", 
                      [{:string => page1, :status => ["200", "OK"], :times => 4},
                       {:string => page2, :status => ["200", "OK"], :times => 1},
                       {:string => page1, :status => ["200", "OK"], :times => 1}])

FakeWeb.register_uri(:get, "http://www.google.com", :string => 
                      "google.com here!", :status => ["200", "OK"])

FakeWeb.allow_net_connect = false

Hier werden zwei unterschiedliche Adressen definiert, die erste mit dem Inhalt zweier vorher durch

curl -is http://www.google.de/ > ./spec/www_google_de.txt
curl -is http://www.google.de/ncr > ./spec/www_google_de_en.txt

erzeugter Dateien. Indem man der Methode register_uri ein Parameterhash übergibt, kann man eine Rotation des Antwortverhaltens erreichen. Es würde also die ersten vier Male der Inhalt der page1 zurückgegeben, danach der von page2 etc. Nachdem das Array einmal “verbraucht” wurde, reagiert das System immer mit dem letzten Eintrag.

Will man auf eine Anfrage mit einem String antworten, so geht dies natürlich auch (siehe google.com).

Um sicherzugehen, dass während der Tests keine anderen Verbindungen geöffnet werden, kann man diese mit allow_net_connect = false blockieren. Der Verbindungsversuch an eine undefinierte Adresse wirft dann einen Fehler.

Der Model-Code, der schließlich die Netzanrufe auslöst, sieht beim mir so aus:


doc = Nokogiri::HTML(open(self.rwatch_url)).css(self.css_selector)

Das open ist einfach das aus open-uri. (Nokogiri ist übrigens ein hervorragender Ersatz für hpricot.)

Weblinks

github.com/chrisk/fakeweb

autotest-Notifications für rspec/cucumber 0

Posted by fwoeck
on Wednesday, June 10

Die Anzeige von Notifications beim Lauf von autospec ist ein hübsches Extra, aber leider durch die unterschiedliche Einbindung in die verschiedenen Umgebungen (bei mir Ubuntu und OsX) ab und zu mit Fallstricken verbunden. Hier ist für beide Welten geschildert, wie’s bei mir läuft:

Ubuntu

Um unter Gnome oder Xfce4 die Ergebnisse ins Desktop einzublenden, benötigt man die libnotify-Bibliothek:

# apt-get install libnotify-bin libnotify1

Danach sollte dieser Test funktionieren:

$ notify-send Test "Testtest, 23..."

Um mit dem Notify-Dienst Kontakt aufzunehmen, müssen ein paar Hooks in die .autotest-Datei gesetzt werden – leider habe ich es bisher nicht hinbekommen, außer den rspec-Meldungen auch die cucumber-Meldungen anzeigen zu lassen. In der ~/.autotest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
require 'autotest/timestamp'
 
module Autotest::GnomeNotify
  def self.notify title, msg, img
    system "notify-send '#{title}' '#{msg}' -i #{img} -t 5000"
  end
 
  Autotest.add_hook :ran_command do |at|
    image_root = "~/.autotest_images"
    r = results = [at.results].flatten.join("\n")
    results.gsub!(/\\e\[\d+m/,'')
    output = results.slice(/(\d+)\sexamples?,\s(\d+)\sfailures?/)
    puts output.inspect
    if output
      if $~[2].to_i > 0
        notify "FAIL", "#{output}", "#{image_root}/fail.png"
      else
        notify "Pass", "#{output}", "#{image_root}/pass.png"
      end
    else
      notify "FAIL", "You must have a non-rspec error in your code.\n
          Check the autotest log for details.", "#{image_root}/fail.png"
    end
  end
 
end

Autotest.add_hook :initialize do |at|
  at.add_exception %r%^\./tmp%
  at.add_exception %r%^\./log%
  at.add_exception %r%^\./db%
end

Insbesondere bei der Verwendung von sqlite-DBs, die unter ./db liegen, hat sich das explizite Ignorieren dieses Verzeichnisses als nötig erwiesen: autospec läuft sonst unaufhörlich, da beim Testlauf die Datenbankfiles geändert werden.

Mac

Die Kombination autotest-fsevent und autotest-growl von BitCetera ist sehr zu empfehlen. autotest-fsevent verwendet auf Leopard-Maschinen die FsEvent-Mechanik und entlastet so die CPU dadurch, dass nicht unaufhörlich nach Fileänderungen gesucht werden muss.

sudo gem install autotest-fsevent
sudo gem install autotest-growl
~ > cat .autotest 
require 'autotest/growl'
# fsevent als letztes(!):
require 'autotest/fsevent'

Tip: ein eventuell zusätzlich vorhandenes ZenTest < 4.1.1 sollte man explizit deinstallieren. Dies killt i.d.R. auch die Binaries einer neueren Version, so dass danach in jedem Falle ein erneutes

gem install ZenTest

fällig ist.

Teststart

Bevor’s losgeht, sollte man noch folgende zwei Variablen in die Systemumgebung bringen (z.B. in der .bashrc):

AUTOFEATURE=true
RSPEC=true

Auf beiden Systemen wird der kontinuierliche jetzt Test so gestartet:

RAILS_ENV=test autospec 2>/dev/null

Weblinks

  1. www.bitcetera.com/.../mac-friendly-autotest/
  2. www.bitcetera.com/.../autotest-fsevent
  3. github.com/svoop/autotest-growl
  4. github.com/svoop/autotest-fsevent

Astrails "safe" kann jetzt auch PostgreSQL 0

Posted by fwoeck
on Saturday, June 06

Das safe-gem der Firma Astrails führt regelmäßige revisionierte Backups von Ordnern und Datenbanken durch. Es bietet einem dabei eine kompakte DSL und kann die Daten wahlweise verschlüsseln und auf S3-Buckets duplizieren.

Kürzlich ist der Support für PostgreSQL-Datenbanken dazugekommen und damit eignet sich die Sache vielleicht für unser Firmenwiki. Ich habe als ersten Test meinen EC2-Host mit diesem Blog aufgetakelt, den ich bisher nur mit manuellen tars und mysqldumps gesichert hatte.

die Installation

Man nimmt direkt die github-Version und erzeugt eine default-Konfiguration:

# gem install astrails-safe --source http://gems.github.com/
Successfully installed astrails-safe-0.1.9
1 gem installed
Installing ri documentation for astrails-safe-0.1.9...
Installing RDoc documentation for astrails-safe-0.1.9...

# astrails-safe /root/my-backup.conf
ERROR: Created default /root/my-backup.conf. Please edit and run again.

Konfiguration

Es folgt eine Beispielkonfiguration in Ausschnitten – das default-File ist gut kommentiert:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
safe do

  local do
    path "/mnt/backup/:kind"
  end

  s3 do
    key "my-Amazon-Key-here"
    secret "my-secret-Amazon-secret-here"
    bucket "the-bucket-name"
    path "backups/:kind/" # this is default
  end

  gpg do
    key "fw@bm.net"
  end

  keep do
    local 4 # keep 4 local backups
    s3 20 # keep 20 S3 backups
  end

  mysqldump do
    options "-ceKq --single-transaction --create-options"

    user "root"
    password "p4ssw0rd"
    socket "/var/run/mysqld/mysqld.sock"

    database :mephisto

  end

  tar do

    archive "etc-files" do
      files "/etc"
    end

    archive "root-files" do
      files "/root"
    end

    archive "mephisto" do
      files "/var/www/mephisto/"
      exclude ["/var/www/mephisto/log", "/var/www/mephisto/tmp"]
    end

  end
end

Wie man sieht, ist das Ganze recht übersichtlich. Damit die gpg-Verschlüsselung funktioniert, benötigen wir ein Schlüsselpaar:

gpg-Schlüsselpaar erzeugen

Anfangs hatte ich die symmetrische Verschlüsselung mit einem Passwort versucht, aber das hat mit der Version 1.9 des gem nicht so recht geklappt – wenn man ein Schlüsselpaar spendiert, funktioniert es aber prima. Das Paar erzeugt man am Besten auf einem anderen Rechner und kopiert nur den öffentlichen Schlüssel auf den Backup-Host.

> gpg --key-gen
...
> gpg -a --export fw@bm.net > fw@bm.net.pub
> scp fw@bm.net.pub root@sat.bm.net:/root/

Auf dem Backup-Host (hier sat.bm.net) wird der öffentliche Schlüssel in den Schlüsselring des Backup-Users importiert (hier root) und als vertrauenswürdig eingestuft:

# gpg --import fw@bm.net.pub
gpg: key 45CA9403: public key "F.W. fw@bm.net" imported
gpg: Total number processed: 1
gpg:               imported: 1

# gpg --edit-key fw@bm.net
...
Command> trust
...
1 = I don't know or won't say
2 = I do NOT trust
3 = I trust marginally
4 = I trust fully
5 = I trust ultimately
m = back to the main menu
Your decision? 5
...
Command> quit

Wer möchte, kann auf dem lokalen Rechner noch eine Kopie des privaten Schlüssels anfertigen – nur für den Fall:

$ gpg -a --export-secret-key fw@bm.net > fw@bm.net.key

die Automation

Das wär’s schon. Es fehlt nur noch die Automation via crond – z.B. morgens, etwa um drei Uhr:

10 3 * * * /opt/ruby-enterprise/bin/astrails-safe /root/my-backup.conf >/dev/null 2>&1

Weblinks

  1. postgress-and-svndump-support-for-astrails-safe-s3-backup
  2. github.com/astrails/safe

10 Minuten-Railssicherheit 0

Posted by fwoeck
on Friday, June 05

Den Vortrag “Security – What rails will and won’t do for you” von Rory McCune im Rahmen von Scotland On Rails 2009 habe ich zum Anlass genommen, den überfälligen Aspekt Sicherheit in meinem laufenden Railsprojekt anzugehen. Der Sprecher benennt XSS- und SQL-Injection-Angriffe als das bei weitem gewichtigste Problem in diesem Zusammenhang.

SQL-Injection

Solange man keine ungeprüften Daten, die potentiell aus Usereingaben stammen in Sql-Statements manuell verpackt, sorgt ActiveRecord dafür, dass alles automatisch sanitized wird. Insofern ist hier Entwarnung angesagt. Konkret heißt das, wenn direktes SQL erzeugt wird, dann NICHT so:

"name='#{@user.name}' and user_id='#{@user.id}'"

sondern SO:

["name='%s' and user_id='%s'", @user.name, @user.id]

Strategien gegen Cross-Site-Scripting

Hier schlägt man uns zwei Möglichkeiten vor:

  1. Prüfung von Userdaten bei der Eingabe
  2. Prüfung bei der Ausgabe

Für beide Varianten gibt es Plugins (siehe die Weblinks). Ich habe mich für die zweite Möglichkeit entschieden und safe-erb installiert:

script/plugin install git://github.com/abedra/safe-erb

Es wirft bei jeder un-sanitizeten Ausgabe eines Strings, der aus DB- oder File-IOs stammt eine Fehlermeldung. Das macht das unbeabsichtigte Übergehen von Lücken schwierig. Die h-Helfermethode Escape-t Tags:

<%=h @user.name %>
oder
<%= h(@user.name) %>

und sorgt dafür, dass tainted Strings untainted werden, d.h. durch die Prüfung kommen.

Zwei Tücken musste ich überwinden:

a) Daten die mit dem Rails-Fragmentcaching in memcached gespeichert werden, werden automatisch tainted, d.h. als potentiell unsicher eingestuft. Das ist nicht hilfreich, weil die Fragmente natürlich Tags enthalten. J. Roller schlägt in seinem Blog folgende Lösung vor, die die Ausgaben direkt in der memcached-Anbindung untaint-et. Sie wird z.B. in einem Initializer eingetragen:

1
2
3
4
5
6
7
8
9
class ActiveSupport::Cache::MemCacheStore   
  logger.info "Installing Memcache-safe_erb Patch"
  
  def read_with_untaint(*args)
    read_without_untaint(*args).untaint
  end
  
  alias_method_chain :read, :untaint
end

b) Bei der Einbettung von Javascript-Schnipseln gab es teilweise Probleme – auch hier darf man natürlich nicht die h-Methode von Rails benutzen, die Tags entfernt, sondern muss den Ergebnisstring untainten:

<%= (observe_field "project_active_#{project.id}", ..., :method => :post).untaint %>

Weblinks

  1. www.mccune.org.uk
  2. github.com/abedra/safe-erb
  3. code.google.com/sanitizeparams
  4. code.google.com/xssterminate
  5. www.owasp.org/Category:OWASP_Top_Ten_Project
  6. www.jroller.com/../fixing_safe_erb_with_memcached

ruby- (und Rails-) Projekte mit dem vim debuggen 0

Posted by fwoeck
on Wednesday, June 03

Anton Astashov hat kürzlich ein Plugin für vim veröffentlicht, mit dem man ruby- und Rails-Projekte aus dem Editor heraus debuggen kann. Mit

:Rdebugger

startet man den Mechanismus. Im Falle eines Railsprojektes wird dann ein Mongrel im development-Modus durch den Debugger gestartet. Auf den kann man wie üblich mit dem Webbrowser zugreifen. Hat man Breakpoints gesetzt, so hält der Prozess an der Stelle an und vim zeigt sie im Hauptfenster.

Nun kann man sich die aktiven Variablen ansehen und schrittweise durch den Quelltext gehen:

Installation

Damit die Sache funktioniert, muss der vim mit “1” antworten, wenn man ihn dies fragt:

:echo has("signs") && has("clientserver") && v:version > 700

Weder der eingebaute vim in Leopard noch der port-vim bringen im Moment eine Unterstützung für das Feature clientserver mit, aber die gvim- und MacVim-Varianten, die ich in Ubuntu und OsX ausprobiert habe, funktionierten beide klaglos.

Außerdem wird das ruby-debug-ide gem genötigt.

Zur Plugininstallation klont man das github-repo:

git clone git://github.com/astashov/vim-ruby-debugger.git

und kopiert dann einige Dateien in vims Pluginverzeichnis:

> cd ./vim-ruby-debugger
> cp -r vim/* ~/.vim/plugin/

Im Hilfetext des Plugins in vim stehen deutliche mehr Informationen, als auf der Hauptseite des github-Projekts:

:helptags ~/.vim/doc
:help ruby-debugger

Entgegen der Voreinstellung habe ich mir die Tastenkürzel auf die Alt-Taste verlegt. In der ~/.vimrc:

map <A-b>  :call g:RubyDebugger.toggle_breakpoint()<CR>
map <A-v>  :call g:RubyDebugger.open_variables()<CR>
map <A-m>  :call g:RubyDebugger.open_breakpoints()<CR>
map <A-s>  :call g:RubyDebugger.step()<CR>
map <A-n>  :call g:RubyDebugger.next()<CR>
map <A-c>  :call g:RubyDebugger.continue()<CR>
map <A-e>  :call g:RubyDebugger.exit()<CR>
map <A-d>  :call g:RubyDebugger.remove_breakpoints()<CR>

let g:ruby_debugger_fast_sender = 1

Die Letzte Zeile ist nötig, um die Benutzung eines Socket-Scripts in C zu aktivieren, dass vermutlich etwas schneller ist, als der übliche Weg (siehe Hilfetext).

Weblinks

github.com/astashov/vim-ruby-debugger

integrity 0.1.10 + rack 1.0.0 + thin 1.2.2 + sinatra 0.9.1.3 0

Posted by fwoeck
on Friday, May 29

Ich hoffe, dass sich die Aktualität dieses Artikels bald erledigt: kurz gefasst, die aktuelle gem-Version von Thin basiert auf rack 1.0.0, die aktuelle integrity-Version auf sinatra 1.9.1.1, welches selbst rack 0.9.x benötigt. Wenn man rack oder Thin oder beide updated, entstehen ein paar Probleme.

Dies ist – soweit ich es jetzt noch zusammenbekomme – wie ich auf meinem Ubuntu-Testserver eine (vermutlich) lauffähige Version zusammengefrickelt habe:

Eine sinatra-Version von SR (s.u.) clonen

Von http://github.com/sr/sinatra/tree/layout-local das git://github.com/sr/sinatra.git clonen und den branch layout-local auschecken:

> git checkout -b layout-local
> vi lib/sinatra/base.rb

dort musste ich für mein REE-1.8.6… die Zeile #46:

inject(0) { |len, part| len + part.bytesize }.to_s
gegen
inject(0) { |len, part| len + part.length }.to_s
ersetzen. Das ist natürlich nicht das Gleiche …

Anschließend noch ein

> rake install

Die letzte integrity-Version clonen

Von http://github.com/integrity/integrity/tree/master den git://github.com/integrity/integrity.git clonen:

> git clone git://github.com/integrity/integrity.git
> cd integrity
> vi integrity.gemspec  # 0.9.1.1 > 0.9.1.3
> gem uninstall integrity
> gem build integrity.gemspec 
> gem install integrity-0.1.10.gem

In der .gemspec muss die Abhängigkeit auf die Version 0.9.1.3 gesetzt werden, damit integrity auch die aktuelle sinatra-Version aufruft.

Weblinks

  1. irclogger.com/integrity/2009-05-19
  2. github.com/integrity/integrity/tree/master
  3. github.com/sr/sinatra/tree/layout-local
  4. http://integrityapp.com/

die Qual der Wahl mit Selenium 1

Posted by fwoeck
on Tuesday, May 26

Wie bringe ich meinem Integrationstest – also hier letztlich Selenium – bei, mehrere Optionen eines Multiselect-Feldes auszuwählen? Die vom SeleniumDriver angebotene Methode select sieht so aus:

select(selectLocator,optionLocator)

wobei der optionLocator soweit ich es sehe nur eine(1) Option zulässt. Das mehrmalige Aufrufen nacheinander klappt leider auch nicht, weil die vorher angewählte Option beim neuen Schritt wieder deaktiviert wird.

Dazu ist mir nur eingefallen, den Wert mit JavaScript zu setzen. Hier benutze ich jQuery, weil es in der Website eh benutzt wird. Mit “run_script” lassen sich Skripte unter dem Driver ausführen. In einer Cucumber-Stepsdefinition sieht das dann so aus:

Given /^I choose "([^\"]*)" and "([^\"]*)" from "([^\"]*)"$/ do |opt1, opt2, field|
  selenium.run_script("$('##{field}').val(['#{opt1}','#{opt2}'])")
end

Der val-Methode von jQuery kann man ein Wertearray übergeben.

Weblinks

  1. selenium.rubyforge.org/rdoc/classes/Selenium
  2. jquery-select-elements-tips-and-tricks

Gotcha: die Klebrigkeit von Operatoren 0

Posted by fwoeck
on Monday, May 25

Einmal mehr…

dies


...select {|group| ( my_rolls.include? group.responsible_id ) && blah.true? }

ist NICHT das Gleiche wie dies:


...select {|group| my_rolls.include? group.responsible_id && blah.true? }

argh.

Hier liegt das Problem:

>> [1,2,3].include? 2 && true
=> false

Klammern machen einen schlanken Fuß:

>> [1,2,3].include?(2) && true
=> true

Hintergrunddienste ordentlich versorgen 0

Posted by fwoeck
on Friday, May 22

Für Railsprozesse, die im Hintergrund asynchron Dinge erledigen sollen, sind ein paar Dinge ganz hilfreich:

  1. Subroutinen zeitlich zu begrenzen und ggf. auftretende Fehler abzufangen
  2. den Dienst selbst auf Steuersignale horchen zu lassen (hier Sig-Term)
  3. adäquate Logeinträge zu prodizieren.

Der folgende Schnipsel zeigt an einer Pdf-Konvertierung, wie man’s machen kann. Er kann als Daemon (in Zusammenhang mit dem daemons-gem) gestartet werden:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env ruby

# You might want to change this
ENV["RAILS_ENV"] ||= "production"

require File.dirname(__FILE__) + "/../../config/environment"

$running = true
Signal.trap("TERM") do 
  $running = false
end

while($running) do
  
  pdfs = QAttachment.all.select {|qa| qa.content_type == 
           'application/pdf' && qa.answer.andand.value_str.blank?}

  pdfs.each do |f| 
    if File.readable?(f.public_filename)
      begin
        timeout(60) do
          f.answer.value_str = `pdftotext -enc UTF-8 -q "#{f.public_filename}" -`
        end
        f.answer.value_str = 'PDF-ERROR' if f.answer.value_str.blank?
      rescue Timeout::Error
        f.answer.value_str = 'PDF-ERROR'
      end
      f.answer.save
      ActiveRecord::Base.logger.info "Pdf-Conversion failed for Attachment
          #{f.answer.id} at #{Time.now}.\n" if f.answer.value_str == 'PDF-ERROR'
    else
      f.destroy
    end
  end

  sleep 10
end

Gotcha: Selenium-Tests ohne Netzwerk 0

Posted by fwoeck
on Thursday, May 21

Wenn ich mein Ubuntu-Laptop so ganz ohne Netzwerkverbindung starte – kein LAN/WAN, entsteht ein nerviges Phänomen: der Firefox, der von Selenium bei einem Integrationstest gestartet wird, hält sich für schlau und startet im Offline-Modus.

Kein Problem, würde man denken: es wird ja eh nur auf der 127.0.0.1 getestet. Ha! Auch lokale Seiten lassen sich nicht aufrufen und ergo meldet Selenium einen Sessionfehler.

Das Anlegen von dummy-Gateways, DNS-Servern etc. hat da nichts gebracht (wie kriegt der Browser eigentlich raus, dass kein WAN da ist?). Aber: wenn man den NetworkManager beendet, ist auf einmal alles gut:

sudo /etc/init.d/NetworkManager stop

Selenium, der ruby-Debugger und OsX 0

Posted by fwoeck
on Tuesday, May 12

Die Kombination cucumber/webrat/Selenium macht mir viel Spaß! Es ist sogar möglich während einer Selenium-Sitzung den ruby-debugger zu starten und mit dem Browser zu interagieren. Auf einer Ubuntu-Maschine könnte man folgende Steps definieren:

Given I follow the link "id=vorgangslink" 
And I follow the link "id=p2show" 
And I follow the link "id=formlink_1" 
And I type "ein" into "id=answer_7" 
And I wait 1 second
And the debugger is started
Then I should see "ein String"

Wobei der Debugger dann so aufgerufen wird:

1
2
3
Given /^the debugger is started$/ do
  debugger 
end

Im konkreten Lauf klappt das prima. Wenn man das Gleiche unter OsX tut, wirft das System einem allerdings eine Fehlermeldung um die Ohren, wenn man das selenium-Objekt anspricht, um den Browser zu steuern:

    Given I follow the link "id=vorgangslink" 
    And I follow the link "id=p2show" 
    And I follow the link "id=formlink_1" 
    And I type "ein" into "id=answer_7" 
    And I wait 1 second
/opt/local/lib/ruby/gems/1.8/gems/cucumber-0.3.3/lib/cucumber/ast/step_invocation.rb:33
status!(:passed)

(rdb:1) selenium.type_keys("id=answer_7", "hey")
NameError Exception: undefined local variable or method `selenium' for #<Cucumber::Ast::StepInvocation:0x36c9f28>

(rdb:1) list
[28, 37] in /opt/local/lib/ruby/gems/1.8/gems/cucumber-0.3.3/bin/...
   28          unless @skip_invoke || options[:dry_run] || exception ...
   29            @skip_invoke = true
   30            begin
...

(rdb:1)

Der Grund liegt darin, dass der OsX-Debugger im Gegensatz zu Linux am Ende eines Blockes bereits in den umliegenden Kontext wechselt, und damit das selenium-Objekt vergisst. Sorgt man durch Anfügen eines nichtstuenden Ausdrucks (hier true) dafür, das das debugger-Kommando nicht das letztze im Block ist, ist alles gut:

1
2
3
4
Given /^the debugger is started$/ do
  debugger 
  true
end

YAML-Files mit UTF-8-Encoding schreiben 0

Posted by fwoeck
on Sunday, May 10

Einmal monatlich möchte(!) ich die Liste aller abgehakten Todos aus der Tracks – Todoliste in unser Zeit-Buchungssystem übertragen. Der kürzeste Weg (es hat keine Web-API) besteht, soweit ich es sehe darin, die einzelnen Tasks nach Datum und Bereich zu gruppieren und als YAML-Datei auszugeben. Der Rest ist Copy-Paste. Leider unterstützt die gewöhnliche ”.to_yaml”-Methode das UTF-8 nur unzulänglich:

>> puts "Wühlmaus".to_yaml
--- "W\xC3\xBChlmaus"

Das ya2yaml-gem schafft Abhilfe:

>> require 'ya2yaml';

>> puts "Wühlmaus".ya2yaml
--- Wühlmaus

Jetzt klappt auch der Export:

1
2
3
4
5
6
7
#!/usr/bin/env /var/www/tracks/script/runner
require 'ya2yaml'

puts Project.find_by_name(ARGV[0]).todos(:oder => 'completed_at ASC').select {|t| 
      t.state == "completed"}.group_by {|t| t.completed_at.to_s[/^2009-([^ ]+) .+$/,1]}.
      map {|k,v| {k => v.group_by {|t| t.context.name }.map {|l,m| {l => m.map{|o| 
      [o.description, o.notes].join(" | ")[/^(.+?)( \| )?$/,1].strip}}}}}.ya2yaml
/var/www/tracks(master) > ./bcs.rb "2009-04" 
--- 
- 
  04-07: 
    - 
      ? "Admin. Infrastruktur" 
      : 
        - Übertragen Fileserverkonf. nach TM
    - 
      Customer-Service:
      ...

Die Google-Analytics API spricht mit dir 0

Posted by fwoeck
on Saturday, May 02

Kurz nach der Veröffentlichung der neuen Google-Analytics-API gibt es jetzt das gem garb, welches die Kommunikation damit erheblich vereinfacht. In diesem Beispiel frage ich die Werte visits, pageviews, etc. für den letzten Monat ab und übergebe sie dem jQuery-Plugin flot, das diese Daten auf eine recht hübsche interaktive Weise darstellt:

Hier folgen die Ergänzungen zu einer leeren Rails-2.3.2-Instanz. Das gesamte Projekt habe ich zum Clonen und Spielen auf Github eingestellt (s.u.).

Die Umgebung

Das gem wird aufgerufen (> 0.2), aus einem YAML-File werden die Accountinformationen eingelesen und eine Session mit Google etabliert.

In config/environment.rb:

1
2
3
4
5
6
7
8
9
10
...
Rails::Initializer.run do |config|
...
  config.gem "vigetlabs-garb", :lib => "garb"
...
end

google_path = File.join(RAILS_ROOT,"config","google.yml")
@@google_config = YAML.load(File.read(google_path))[RAILS_ENV]
Garb::Session.login(@@google_config['login'], @@google_config['password'])

Anmeldedaten

Das File config/google.yml enthält die Domaindaten:

1
2
3
4
5
6
7
8
9
development:
  domain: rnotes.bm.net
  login: me@bm.net
  password: p4ssw0rd

production:
  domain: rnotes.bm.net
  login: me@bm.net
  password: p4ssw0rd

Controller-Code

Wenn der Welcome-Index aufgerufen wird, wird die Datenabfrage im Controller assembliert und aufgerufen. Einer sauberen MVC-Kultur folgend, sollte man dies in einem richtigen Projekt vielleicht in ein Modell geben.

app/controllers/welcome_controller.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class WelcomeController < ApplicationController

  def index
    profile = Garb::Profile.all.select {|p| p.title == 
                              @@google_config['domain']}.first
    report = Garb::Report.new(profile, :start_date => 
                              4.weeks.ago, :end_date => Time.now )

    report.metrics :pageviews, :visits, :visitors, :new_visits
    report.dimensions :date
    report.sort :date
    result = report.results

    @visitors = result.map(&:visitors)
    @visits = result.map(&:visits)
    @newvisits = result.map(&:new_visits)
    @pageviews = result.map(&:pageviews)
    @dates = result.map {|d| d.date.to_time.to_i * 1000 }
    @domain = @@google_config['domain']
  end

end

Hier ist bemerkenswert, wie die Datumsformate “CCYYMMDD”, die von Google als String kommen in Javascript-Timestamps umgerechnet werden, wie flot sie erwartet.

Die Diagrammdarstellung

Im Index werden die Ergebnisse ein ein Javascript eingefügt, das letztlich flot benutzt. Weitere Beispiele mit verschiedenen Optionen findet man auf der Website des Plugins.

Das app/views/welcome/index.html.erb-File:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<h1><%= @domain -%></h1>

<p><br /></p>

<div id="placeholder" style="width:600px;height:300px"></div>
<div id="overview" style="margin-left:200px;margin-top:20px;
                                           width:400px;height:50px"></div>

<script id="source" language="javascript" type="text/javascript">
$(function () {
    var d1 = [<%= i = -1; @visitors.map {|v| i += 1; 
                        "[#{@dates[i]}, #{v}]" }.join(", ") -%>];
    var d2 = [<%= i = -1; @visits.map {|v| i += 1; 
                        "[#{@dates[i]}, #{v}]" }.join(", ") -%>];
    var d3 = [<%= i = -1; @pageviews.map {|v| i += 1; 
                        "[#{@dates[i]}, #{v}]" }.join(", ") -%>];
    var d4 = [<%= i = -1; @newvisits.map {|v| i += 1; 
                        "[#{@dates[i]}, #{v}]" }.join(", ") -%>];
    var d = [ { label: 'Visitors', data: d1 }, { label: 'Visits', data: d2 }, 
        { label: 'Pageviews', data: d3 },  { label: 'New visits', data: d4 } ];

    // helper for returning the weekends in a period
    function weekendAreas(axes) {
        var markings = [];
        var d = new Date(axes.xaxis.min);
        // go to the first Saturday
        d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 1) % 7))
        d.setUTCSeconds(0);
        d.setUTCMinutes(0);
        d.setUTCHours(0);
        var i = d.getTime();
        do {
            // when we don't set yaxis the rectangle automatically
            // extends to infinity upwards and downwards
            markings.push({ xaxis: { from: i, to: i + 2 * 24 * 60 * 60 * 1000 } });
            i += 7 * 24 * 60 * 60 * 1000;
        } while (i < axes.xaxis.max);

        return markings;
    }
    
    var options = {
        xaxis: { mode: "time" },
        selection: { mode: "x" },
        legend: { position: 'sw' },
        points: { show: true },
        lines: { show: true },
        grid: { markings: weekendAreas }
    };

    var plot = $.plot($("#placeholder"), d, options);

    var overview = $.plot($("#overview"), [d1, d2, d3, d4], {
        lines: { show: true, lineWidth: 1 },
        shadowSize: 0,
        xaxis: { ticks: [], mode: "time" },
        selection: { mode: "x" }
    });

    $("#placeholder").bind("plotselected", function (event, ranges) {
        // do the zooming
        plot = $.plot($("#placeholder"), d,
                      $.extend(true, {}, options, {
                          xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }
                      }));

        // don't fire event on the overview to prevent eternal loop
        overview.setSelection(ranges, true);
    });

    $("#overview").bind("plotselected", function (event, ranges) {
        plot.setSelection(ranges);
    });
});
</script>

Javascripts und JRails

flot selbst benötigt jQuery. Eine elegante und schnelle Art der Einbettung ist es, das jRails-plugin zu installieren – es bringt jQuery mit:

script/plugin install http://ennerchi.googlecode.com/svn/trunk/plugins/jrails

Das fügt die rechten Files in public/javascripts ein. Es fehlt nur noch das jquery.flot.js:

  1. jrails.js
  2. jquery-ui.js
  3. jquery.flot.js
  4. jquery.js

das html-Layout

Zum Schluss müssen die Scripte im Layout aktivert werden – app/views/layouts/application.html.erb:

1
2
3
4
...
  <%= javascript_include_tag :defaults %>
  <%= javascript_include_tag 'jquery.flot' %>
...

Weblinks

  1. github.com/fwoeck/analytics
  2. introducing-garb-access-the-google-analytics-data-export-api-with-ruby
  3. github.com/vigetlabs/garb
  4. code.google.com/p/flot
  5. code.google.com/gdataReferenceDimensionsMetrics

Sehr cooles Debug- und Analysetool: rack-bug 0

Posted by fwoeck
on Wednesday, April 29

Die Rackifizierung greift um sich! Hier sehen wir eine Folge: Bryan Helmkamp hat grade sein an ein Django-Tool angelehntes rack-bug vorgestellt. Es ist eine Rackmiddleware, die ein Statusfenster in die laufende Website einblendet. Es gibt eine Menge sehr interessanter Möglichkeiten, z.B. die Auswertung

  1. der verbrauchten CPU-Zeiten en Detail
  2. der Session- und Cookievariablen
  3. der ENV- und Headerumgebung
  4. der SQL-Statements mit Backtrace, Explain, Timings und der Wiederholung einzelner Selects
  5. der Cachinhalte

und mehr!

Die Installation

... ist trivial (hier für Rails):

script/plugin install git://github.com/brynary/rack-bug.git

Anschließend ruft man die Middleware auf. In der config/initializers/middleware.rb:

require "rack/bug" 

ActionController::Dispatcher.middleware.use Rack::Bug,
  :ip_masks   => [IPAddr.new("127.0.0.1")],
  :secret_key => "KsdUrsfdY7sdfDEsdf52sdfhD4yWY+8z1",
  :password   => "p4ssw0rd"

Nun lässt sich das Tool auf der Unterseite /__rack_bug__/bookmarklet.html des Projekts aktivieren.

Weblinks

  1. www.brynary.com/rack-bug-debugging-toolbar-in-four-minutes
  2. github.com/brynary/rack-bug