Pivotal Labs

Main menu

Skip to primary content
Skip to secondary content
  • About
  • Case Studies
  • Team
    • Executives
    • Locations
      • San Francisco (HQ)
      • Boston
      • Boulder
      • Denver
      • London
      • Los Angeles
      • New York
  • Community
    • Blogs
    • Tech Talks
    • Events
  • Careers
    • Lifestyle
    • Principles & Practices
    • Benefits
    • FAQ
    • Apply
  • Contact
    • Press Room
    • Press Releases
    • In The News
    • Press Kit
  • All
  • Labs
  • Standup
  • Tracker

Monthly Archives: September 2012

Laurence Koret

Google Hangouts an improvement over Skype

Laurence Koret
Friday, September 28, 2012

The Problem

Skype connectivity sometimes drops calls. It is also challenging to schedule with multiple people. We already use Google Apps so it made sense since it would integrate with our other systems so easily.

The Fix

We having using Google Hangouts and have been very pleased with the results so far.

The setup is more involved than using Skype so I am going to detail the steps involved.

3 Things have to be done in order to use a Hangout.

  1. You first must be a member of Google+. If you are doing this for your @pivotallabs.com Google identity make sure you are signing up with your corporate email address and not your personal Google account. Join at plus.google.com.

  2. Enable the Google Video Chat plugin or download from here.

  3. Start a Hangout

    • Go to plus.google.com/hangouts and click the Start a Hangout button on the top right side of your screen.
    • Click the Hangouts icon underneath an interesting post on your Home page to start a hangout about the post.
    • Click the Hangouts icon Google Hangout Logo on the left side of the page and click Start a hangout under the ‘HANGOUT INVITE’ section.
    • You can also start a hangout and send and receive hangout invites from other Google properties including:
      Google Chat properties (ie. Gmail, Google+, iGoogle, orkut) Learn more.
    • Google Calendar Learn more.

Neat things you can do with a Hangout

Schedule a recurring meeting with the same Hangout address

This is very handy for a meeting which happens repeatedly so that you always have the same URL to join, like a standup meeting. This was first found by Joe Moore from Pivotal on this blog entry by David Cummings

The steps are as follows:

  • Create a new event on Google+ Events.
  • Give the event a date far into the future, like the year 2020
  • Go to Event options -> Advanced and click on Google+ Hangout
  • Save the event
  • Share the link to the Google+ Hangout on your repeating Google Calendar event

Get a better quality output if you are doing a live music Hangout using Studio Mode.

Create a Hangout very quickly by using the chat window when signed into your Google email and clicking on the Hangout icon or create an event on your Google calendar and add a Hangout to the event.

Improved Camera

We also found that upgrading the webcam you are using can help to improve your experience. We have upgraded to the Logitech HD Pro Webcam C910. This version is Mac compatible, the newer model which is the C920 is not. The 910 provides improved optics with a better lens and an improved depth of field. This version is also easily portable if you have a laptop.

Summary

We only expect the Google Hangout to get better as Google keeps working to improve it. So far it has been a big improvement over Skype.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Matthew Parker

Injectable Persistence Layers – a Refactoring Step by Step

Matthew Parker
Friday, September 28, 2012

This week I worked on a new web interface for our open source project License Finder.

The license_finder gem persists dependency information out to a YAML file; however, we wanted to persist these same
dependency objects to a SQL database for the website.

Step 1: Persistence base class

In order to accomplish this, I had to perform a series of refactorings that would make it possible to swap out YAML persistence
with an ActiveRecord persistence layer. The LicenseFinder::Dependency class had never been setup make persistence injectable,
so the first step was moving all persistence-related functionality out into a seperate base class, and giving it an ActiveRecord-style API:

module LicenseFinder
  module Persistence
    class Dependency
      class Database
        #... YAML 'database' implementation details
      end

      attr_accessor *LicenseFinder::DEPENDENCY_ATTRIBUTES

      class << self
        def find_by_name(name)
          attributes = database.find { |a| a['name'] == name }
          new(attributes) if attributes
        end

        def delete_all
          database.delete_all
        end

        def all
          database.all.map { |attributes| new(attributes) }
        end

        def unapproved
          all.select {|d| d.approved == false }
        end

        def update(attributes)
          database.update attributes
        end

        def destroy_by_name(name)
          database.destroy_by_name name
        end

        private
        def database
          @database ||= Database.new
        end
      end

      def initialize(attributes = {})
        update_attributes_without_saving attributes
      end

      def config
        LicenseFinder.config
      end

      def update_attributes new_values
        update_attributes_without_saving(new_values)
        save
      end

      def approved?
        !!approved
      end

      def save
        self.class.update(attributes)
      end

      def destroy
        self.class.destroy_by_name(name)
      end

      def attributes
        attributes = {}

        LicenseFinder::DEPENDENCY_ATTRIBUTES.each do |attrib|
          attributes[attrib] = send attrib
        end

        attributes
      end

      private
      def update_attributes_without_saving(new_values)
        new_values.each do |key, value|
          send("#{key}=", value)
        end
      end
    end
  end
end

With all of the persistence-related functionality in this base class, I could now update the LicenseFinder::Dependency class to inherit from this:

module LicenseFinder
  class Dependency < LicenseFinder::Persistence::Dependency
    #...
  end
end

I also created a shared example for describing how persistence should work (regardless of the underlying persistence implementation):

shared_examples_for "a persistable dependency" do
  let(:klass) { described_class }

  let(:attributes) do
    {
      'name' => "spec_name",
      'version' => "2.1.3",
      'license' => "GPLv2",
      'approved' => false,
      'notes' => 'some notes',
      'homepage' => 'homepage',
      'license_files' => ['/Users/pivotal/foo/lic1', '/Users/pivotal/bar/lic2'],
      'readme_files' => ['/Users/pivotal/foo/Readme1', '/Users/pivotal/bar/Readme2'],
      'source' => "bundle",
      'bundler_groups' => ["test"]
    }
  end

  before do
    klass.delete_all
  end

  describe '.new' do
    subject { klass.new(attributes) }

    context "with known attributes" do
      it "should set the all of the attributes on the instance" do
        attributes.each do |key, value|
          if key != "approved"
            subject.send("#{key}").should equal(value), "expected #{value.inspect} for #{key}, got #{subject.send("#{key}").inspect}"
          else
            subject.approved?.should == value
          end
        end
      end
    end

    context "with unknown attributes" do
      before do
        attributes['foo'] = 'bar'
      end

      it "should raise an exception" do
        expect { subject }.to raise_exception(NoMethodError)
      end
    end
  end

  describe '.unapproved' do
    it "should return all unapproved dependencies" do
      klass.new(name: "unapproved dependency", approved: false).save
      klass.new(name: "approved dependency", approved: true).save

      unapproved = klass.unapproved
      unapproved.count.should == 1
      unapproved.collect(&:approved?).any?.should be_false
    end
  end

  describe '.find_by_name' do
    subject { klass.find_by_name gem_name }
    let(:gem_name) { "foo" }

    context "when a gem with the provided name exists" do
      before do
        klass.new(
          'name' => gem_name,
          'version' => '0.0.1'
        ).save
      end

      its(:name) { should == gem_name }
      its(:version) { should == '0.0.1' }
    end

    context "when no gem with the provided name exists" do
      it { should == nil }
    end
  end

  describe "#config" do
    it 'should respond to it' do
      klass.new.should respond_to(:config)
    end
  end

  describe '#attributes' do
    it "should return a hash containing the values of all the accessible properties" do
      dep = klass.new(attributes)
      attributes = dep.attributes
      LicenseFinder::DEPENDENCY_ATTRIBUTES.each do |name|
        attributes[name].should == dep.send(name)
      end
    end
  end

  describe '#save' do
    it "should persist all of the dependency's attributes" do
      dep = klass.new(attributes)
      dep.save

      saved_dep = klass.find_by_name(dep.name)

      attributes.each do |key, value|
        if key != "approved"
          saved_dep.send("#{key}").should eql(value), "expected #{value.inspect} for #{key}, got #{saved_dep.send("#{key}").inspect}"
        else
          saved_dep.approved?.should == value
        end
      end
    end
  end

  describe "#update_attributes" do
    it "should update the provided attributes with the provided values" do
      gem = klass.new(attributes)
      updated_attributes = {"version" => "new_version", "license" => "updated_license"}
      gem.update_attributes(updated_attributes)

      saved_gem = klass.find_by_name(gem.name)
      saved_gem.version.should == "new_version"
      saved_gem.license.should == "updated_license"
    end
  end

  describe "#destroy" do
    it "should remove itself from the database" do
      foo_dep = klass.new(name: "foo")
      bar_dep = klass.new(name: "bar")
      foo_dep.save
      bar_dep.save

      expect { foo_dep.destroy }.to change { klass.all.count }.by -1

      klass.all.count.should == 1
      klass.all.first.name.should == "bar"
    end
  end
end

Step 2 – Make persistence autoloadable

Next, I wanted to make persistence autoloadable in the gem (so that other persistence solutions could simply create their own
LicenseFinder::Persistence::Dependency implementation before doing a require "license_finder":

module LicenseFinder
  module Persistence
    autoload :Dependency, 'license_finder/persistence/yaml/dependency'
    autoload :Configuration, 'license_finder/persistence/yaml/configuration'
  end
end

Step 3 – Create new persistence implementation

Now, creating an ActiveRecord persistence implementation was as simple as:

module LicenseFinder
  module Persistence
    class Dependency < ActiveRecord::Base
      serialize :license_files
      serialize :readme_files
      serialize :bundler_groups
      serialize :children
      serialize :parents

      belongs_to :config

      scope :unapproved, where(approved: false)
    end
  end
end

require "license_finder"

And the test for this persistence implementation:

require "spec_helper"
require_relative "path/to/LicenseFinder/spec/support/shared_examples/persistence/dependency.rb"

describe LicenseFinder::Persistence::Dependency do
  it_behaves_like "a persistable dependency"
end
  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Alex Stupakov

[Standup][SF] 09/28/12: 10 orders of magnitude faster!

Alex Stupakov
Friday, September 28, 2012

Interestings

  • rbenv init not multi-shell safe

eval “$(rbenv init -)” does an rbenv rehash which only allows one session in at a time.

This causes weird failures if you have multiple CI builds starting at the same time.

Having one master build do the regular init, and then the other builds do a eval “$(rbenv init – –no-rehash)” solved it for us.

  • RVM binary rubies rock

Not needing to compile rubies after download makes rvm way faster

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
JT Archie

[NYC][Standup] 09/27/12: Protect all your attributes

JT Archie
Thursday, September 27, 2012

Interestings

attr_accessible is gone

The strong_parameters gem has been integrated into rails edge by DHH and replaces attr_accessible.

https://github.com/rails/rails/commit/c49d959e9d40101f1712a452004695f4ce27d84c

Capybara’s should_not have_css visibility: false

Capybara’s “should_not have_css “#whatevs”, visibility: false” results in flaky tests if the content in question is being hidden after a process completes (such as an AJAX request).

An alternative is “should_not have_selector “#whatevs”, visible: false”. This results in substantially less flaky integration tests.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Brian Cunnie

Shunting Ethernet Guests to a "Safe" Network

Brian Cunnie
Thursday, September 27, 2012

Abstract

On occasion a non-employee will need to connect their laptop to our ethernet network, which begs the question, “How do we allow customers to access our network while protecting our workstations?”

The short answer is that we use a combination of VMPS-capable switches, VMPS software, VLANs, DNS, and DHCP. And, of course, reasonably stringent firewall rules.

We are a Software Services company, and at any given moment 40% of the 200-odd people in our San Francisco office are not employees. Of those 80 people, 98% of them can access the guest WiFi network without a problem. There are, however, the remaining 2% who, for whatever reason (their WiFi chipset doesn’t interoperate well with our WiFi Access Points, their wireless is broken, they’ve accidentally deleted their drivers, etc…) cannot connect to the WiFi. They need to access the Internet, and they can only use ethernet.

We want to give our guests ethernet connectivity when needed, but not in such a way that it jeopardizes the security of our workstations.

Audience

This article is directed to IT organizations

  • That have smart switches that are VMPS-capable
  • That have *NIX-based DNS & DHCP servers
  • That have guests that need need ethernet access
  • That have a requirement to quarantine their guests’ machines
  • That are willing to record the MAC address of every device on their network (i.e. not their guests’ devices, just their own)

Steps

These are the steps to go through.

First, we assume you have already set up your VLANS, and have entered them into your ethernet switch(es). These are our VLANs (note: the IP addresses and subnet masks are simplified for purposes of our discussion):

VLAN    Name            IP
1       default         10.0.1.0/24
2       SERVER          10.0.2.0/24
3       PAIRING_DMZ     10.0.3.0/24
4       VOIP            10.0.4.0/24
5       PIVOTAL_WIFI    10.0.5.0/24
6       PIVOTAL_GUEST   10.0.6.0/24
7       SECURITY        10.0.7.0/24
8       COMMON          10.0.8.0/24

Note VLAN 6 (PIVOTAL_GUEST); this is the VLAN we’ll use to quarantine our guests.

Secondly, you’ll need to configure your switches. In our case, we have Cisco 2960G 48-port switches, which requires enabling both VTP and VMPS.

We’ll need to configure one switch as the VTP server, and the remaining switches as the VTP clients. We used the following commands to configure the server:

sw-00#config term
Enter configuration commands, one per line.  End with CNTL/Z.
sw-00(config)#vtp mode server
sw-00(config)#vtp version 2
sw-00(config)#vtp domain sf.pivotallabs.com
sw-00(config)#vmps server 10.0.1.16 primary
sw-00(config)#end

You’ll need to configure the remaining switches as follows:

sw-01#config term
Enter configuration commands, one per line.  End with CNTL/Z.
sw-01(config)#vtp mode client
sw-01(config)#vmps retry 5
sw-01(config)#vmps server 10.0.1.16 primary
sw-01(config)#end

Then you’ll need to set up your VMPS server on your *NIX box:

  • The VMPS server must be reachable from every switch on the network. In our case, we decided to run the VMPS daemon on our DNS/DHCP server (10.0.1.16).
  • We used Dori Seliskar’s OpenVMPS. It installed fairly easily on our FreeBSD 8.3 machine.

The commands to install:

curl -L http://sourceforge.net/projects/vmps/files/latest/download  | tar xzvf -
cd vmpsd-1.4.04
bash configure
make
sudo make install

VMPS Server Configuration

We replaced the VMPS server configuration file (/usr/local/etc/vlan.db) with the following (truncated (we only show 8 address records of the full 381) and edited for readability):

vmps domain sf.pivotallabs.com
vmps mode open
vmps fallback PIVOTAL_GUEST
vmps no-domain-req deny

vmps-mac-addrs

! address <addr> vlan-name <vlan_name> ! comment
address 0022.4d6b.dead vlan-name SECURITY ! nvr
address 3c07.545c.beef vlan-name CUST_2 ! bartol
address 001f.f352.dead vlan-name default ! adair
address c82a.1414.beef vlan-name PAIRING_DMZ ! aerial
address f0de.f134.dead vlan-name FINANCE ! bill-thinkpad
address 001b.781d.beef vlan-name COMMON ! goldfinger
address 0004.f234.dead vlan-name VOIP ! voip-ash

The important things to note about this file are the following:

  • You should customize the VMPS domain (i.e. sf.pivotallabs.com) to match your site. It must also match the VTP domain configured on your switches. You are not required to use DNS domain-format.
  • The fallback PIVOTAL_GUEST directive is crucial: it shunts all unrecognized MAC addresses onto the PIVOTAL_GUEST VLAN.
  • The VLANs (e.g. PIVOTAL_GUEST, VOIP, COMMON) must be defined on the switches; use the IOS command show vlan to determine which VLANs have been defined.
  • The MAC addresses are in Cisco notation (e.g. c82a.1414.beef) not IEEE 802 notation (e.g. c8:2a:14:14:be:ef).
  • An exclamation mark (”!“) and everything following it are ignored (i.e. used for comments).
  • We do not record the MAC addresses of our WiFi clients; by connecting to the WiFi they are automatically restricted to the appropriate VLAN (PIVOTAL_WIFI in the case of employees, PIVOTAL_GUEST in the case of guests).

Customizations

We additionally did the following:

  • wrote a start-up script so that the vmpsd daemon would start on reboot
  • wrote a script which created the vlan.db based on the MAC addresses culled from our DHCP tables
  • modified our Makefile (we use make to build our DNS & DHCP files) to include the building of our VMPS file (vlan.db)

Gotchas

  • Treat your vlan.db file with care. On the second day of our roll-out, we accidentally truncated our vlan.db file. The effect was the a subset of people lost connectivity (their workstations had been shunted off to the guest VLAN, but retained the IP address from their previous VLAN (their DHCP lease had not expired). Net result: it was as if someone had yanked out their ethernet cable).
  • Small desktop switches are unusable if two of the devices on the switch are on different VLANs. For example, we plugged an IP Phone (VLAN 4, VOIP) and a Mac Mini (VLAN 1, PIVOTAL) into the same desktop switch, and made a phone call while doing a download. Our experience: the download would freeze while the phone conversation was fine. A few seconds later, the phone conversation would cut off while the download suddenly started up again. A few seconds after that, the phone call would resume and the download would freeze.
  • Similarly, a user of virtualization software (e.g. VMware, VirtualBox, Xen, Linux KVM) who bridges (instead of NATs) their VM’s network interface will suffer as the ethernet switch ping-pongs their interface between the guest network and their normal network.
  • This [solution] is not Fort Knox. For example, a canny hacker could clone a MAC address of one of our workstations and use that to access our network.
  • It takes a fair amount of IT discipline to record the MAC address of every ethernet device.

Bibliography

  • Cisco 2960 Documentation

Acknowledgements

I would like to thank Michael Sierchio for doing the lion’s share of the work, and Colin Deeb for fixing problems during the roll-out.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Rob Mee

You Asked. We Listened: New Services, Locations and New Faces

Rob Mee
Thursday, September 27, 2012

Earlier in the year I said that joining EMC would turbocharge Pivotal Labs’ business, and that is certainly proving to be the case. Today I am very pleased to announce the launch of Pivotal’s new product management practice, expanded design services, and the opening soon of two regional offices in Boston and Los Angeles, as well as our first office in Europe, to be based in London. To help guide the company during this growth stage we have appointed several executives to lead key business functions.

Our product management and design services can now help clients plan, define, prioritize, and design their entire product and strategic roadmap to promote greater efficiency throughout the entire development process. We can take great satisfaction in knowing we are helping our clients build quality products and deliver them to market faster through our focus on a creative culture and uniquely disciplined processes.

No doubt those of you who spend any appreciable time at Pivotal will have noticed a few new faces in the hallways. I am delighted to formally announce several executive appointments:

  • Edward Hieatt, who has served as our Vice President of Engineering, has been promoted to Chief Operating Officer;
  • Matt Eng has been appointed Chief Financial Officer;
  • Drew McManus will lead the new design and product management practice as Vice President of Product;
  • Melissa Dyrdahl has joined as Vice President of Marketing.

At the end of the day it’s all about delivering results faster while continuing to focus on improving the customer experience. Our clients have asked Pivotal for these new services, and we are proud to be able to deliver them.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Cathy O'Connell

NYC Stand-up Wednesday 26th September

Cathy O'Connell
Wednesday, September 26, 2012

Interestings

FuzzBert, a minimal random testing framework

From Martin Bosslet, the man who brought you Krypt, comes an RSpec-compatible library for generating random data for tests. Not like Faker, more like “random streams of garbage to test your parsing code”.
FuzzBert

Jetstrap, an interface builder for Twitter Bootstrap

Jetstrap is a drang-and-drop interface builder to generate symantic markup using Twitter Bootstrap components.

NYC.rb Hack Night releases: hoe-debugging and hoe-bundler

Coded at hack night:

Hoedebugger
New point release of hoe-debugger allows C extension developers to generate valgrind suppression files for their rubies; and automatically use them when valgrinding.

Hoebundler
Bugfix release of hoe-bundler handles duplicate dependency declarations gracefully.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Christian Niles

Pivotal Tracker on the iPhone 5

Christian Niles
Tuesday, September 25, 2012

As if you haven’t found enough excuses to buy a new iPhone 5, here’s another: Pivotal Tracker for iOS version 1.5.2 was released today and includes support for the iPhone 5! With the extra screen space, you’re able to see an extra story and a half per tab without scrolling:

This release also includes some other updates for iOS 6.0, which over 60% of our users are now running after just 5 days. 9% of our users already have an iPhone 5!

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Nate Clark

09/25/12: It’s about something

Nate Clark
Tuesday, September 25, 2012

Helps

  • Where are my backtraces in specs for gems? (backtrace_silencer is off)

RubyMine uses a custom formatter.
Try rspec my_spec.rb -b on the command line.

Interestings

  • If you new up a Rails Logger in your Unicorn after_fork, that is the logger you get

If you do something like this:
after_fork do |server, worker|
current_directory = File.expand_path(File.dirname(FILE))
log_path = “#{current_directory}/log/#{Rails.env}.#{worker.nr}.log”
Rails.logger = ActiveSupport::BufferedLogger.new(log_path)
Rails.logger.level = Logger::INFO
end

… in order to have a different logfile for each unicorn worker, setting the log level in your environment file will not take affect.

  • any_instance doesn’t work with should_not_receive

It always passes.

The rspec folks haven’t decided what it should do. “Official” suggestion is to only use any_instance with stubbing instead of with expectations.

  • Capistrano doesn’t clean up that well

When you use the deploy:cleanup task it only looks at the FIRST server declared in deploy.rb to determine which releases to rm. You’d think that it would look at the primary server. But then you’d think that the task would be included by default, too. Which it doesn’t.

  • Enumerators, ActiveRecord Connections, and JRuby

We ran into what we thought was a quirk of Enumerators where inside the enumerator you are in a different thread from outside. This affects ActiveRecord because your connection is determined by Thread.current.object_id. So you’d end up with a new connection inside the Enumerator.

This turned out to only be true in JRuby. Under MRI, you have the same Thread object, however, you have a new Thread locals context. This makes it particularly hard to find someplace cross Ruby for Rails to store its connection id.

https://jira.codehaus.org/browse/JRUBY-6887

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Alex Stupakov

[Standup][SF] 09/24/12: Everyone agrees 2-factor auth is good? Yes.

Alex Stupakov
Monday, September 24, 2012

Helps

  • Apple push notification gem

What is the best gem to use for Apple push notifications from a Rails 3.2 server when Urban Airship is not an option?

(nobody knows)

  • Is Google 2-Step Authentication a good thing?

After all the cracking stories in the news of late, I decided to turn on Google’s 2-step authentication. It’s a little annoying to wait for a 6-digit code to arrive on my cell phone when I want to log in, but it beats losing my account to the forces of evil.

Any warnings or experiences, good or bad?

Response: Most everyone agrees: yes, this is good, use it.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter

Topics

  • agile (781)
  • rails (113)
  • testing (88)
  • ruby (83)
  • ruby on rails (70)
  • jobs (62)
  • javascript (55)
  • techtalk (44)
  • rspec (38)
  • ironblogger (32)
  • productivity (30)
  • activerecord (29)
  • gogaruco (29)
  • git (28)
  • nyc (27)
  • rubymine (26)
  • bloggerdome (23)
  • mobile (22)
  • process (21)
  • pivotal tracker (21)
  • cucumber (20)
  • design (19)
  • jasmine (19)
  • ios (18)
  • webos (17)
  • objective-c (17)
  • android (16)
  • tracker ecosystem (16)
  • palm (16)
  • "soft" ware (16)
  • fun (15)
  • ci (15)
  • cedar (15)
  • rails3 (14)
  • performance (14)
  • bdd (14)
  • gem (13)
  • css (13)
  • tdd (13)
  • selenium (12)
  • goruco (12)
  • bundler (12)
  • meetup (11)
  • railsconf (11)
  • nyc-standup (11)
  • capybara (10)
  • mac (10)
  • mojo (10)
  • chef (10)
  • api (10)
Subscribe to Community Feed
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. →
  • About
  • Case Studies
  • Team
  • Community
  • Careers
  • Contact
  • Labs
  • Events

Contact Us

contact@pivotallabs.com
+1 415-77-PIVOT
TwitterLinkedInFacebook

Pivotal Tracker

Tracker is the award-winning agile project management tool that enables real-time collaboration around a shared, prioritized backlog.
Visit pivotaltracker.com >