There was one ad in a recent edition of Time Magazine that caught my eye. We've been trying to figure out how to best market Postica since officially going into production in July. I'm pretty skeptical about the effectiveness of advertising in general. Since we're bombarded with so much of it, I think most people simply ignore it. But every once in a while, some advertisement will catch my eye. I don't know if the fact that I read it means that it actually increases sales of the product advertised, but it obviously can't be worse than an ad that nobody reads.
The full-page ad in Time was for men's white dress shirts. Just plain, white button-down shirts that I buy every 5 years from Ross when the one I bought five years previously no longer fits. I had never heard of the brand; it was a man's name (two first names, I think). It was one particular shirt, rather than an ad for the brand in general.
What made me read the ad was simply that I wouldn't expect to see such an ad in Time Magazine. I wonder how sales of that shirt are doing. I believe one could order the shirt in question online or by calling an 800 number, similar to ads for overpriced cds composed of the Best Love Songs of Some Period within Some Genre or "collectible" coins that aren't legal tender.
Does placing an ad where potential customers wouldn't expect to see it help sales?
business » marketing » advertising | Permanent Link
I sold half my Apple stock at $61.33 today. It's pretty damn expensive at this level.
Some web hosting customers, especially those with dubious business plans who have hired SEO witch doctors, request that every web site be placed on a separate IP address. The theory is that Google is likely to discount the links between sites on the same IP address because such sites may be trying to artificially increase their PageRank. It would be silly for Google to do this since there could be hundreds or thousands of unrelated sites that share an IP address, simply because they are customers of the same web hosting company. Nonetheless, this seems to be a widely-held belief.
This is frustrating to those in the hosting business because IP(v4) addresses are a finite resource, and here in North America, requests for new IP addresses for IP-based web hosting must include a technical justification.
Today, Sean emailed Google to ask for clarification. I hope that Google will publicly state that using multiple IP addresses cannot be used to inflate PageRank.
Update (Thu, 09 Dec 2004): Google responds by saying, go ask someone else to speculate.
Date: Thu, 09 Dec 2004 14:33:35 -0800 From: help@google.com Subject: Re: [#17061378] Regarding the need for a static IP Address Hi Sean, Thank you for writing to us. Please note that we don't comment on webmaster techniques or the details of our search technology beyond what appears on our site. We've dedicated an entire section of our site to answering the most common questions from those who maintain and/or promote websites. You'll find all of our publicly available information posted at http://www.google.com/webmasters/index.html Besides this section of our site, we've created a newsgroup discussion forum for passionate Google users. At http://groups.google.com in the http://groups.google.com/groups?q=google.public.support.general group, many webmasters and Google users share their questions and expertise. We recommend performing an advanced search on this group if you feel your question is particularly challenging and you've been unable to find an answer on our site. To do so, go to http://www.google.com/advanced_group_search?hl=en and enter your search terms in one of the 'Find messages' fields at the top of the page. Type 'google.public.support.general' in the 'Newsgroup' search field, and click 'Google Search.' If you don't find an answer to your question, you can always post your question to the group to see if other newsgroup users have helpful advice. Please be aware that this content isn't posted by Google, and we cannot verify its accuracy. Regards, The Google Team
Also, a good example of the READ-COMMITTED isolation level
mod_accounting is a simple
Apache module for recording bandwidth usage on a per-virtual host basis. It
can write to either a MySQL or PostgreSQL database. You just add a couple
directives to httpd.conf to tell mod_accounting which database to write to, the
query to run, and how often to write to the database.
Here's the configuration I use.
<IfModule mod_accounting.c>
AccountingQueryFmt " \
INSERT INTO \
vhost_accounting \
( \
bytes_in, \
bytes_out, \
virtual_host, \
host \
) \
values \
( \
%r, \
%s, \
LOWER('%h'), \
'host.example.com' \
);"
AccountingTimedUpdates 300
AccountingDatabase vhost_accounting
AccountingDatabaseDriver mysql
AccountingDBHost stats.example.com 3306
AccountingLoginInfo vhost_accounting XXXXXXXX
</IfModule>
And the corresponding table.
CREATE TABLE `vhost_accounting` ( `virtual_host` varchar(128) NOT NULL default '', `bytes_in` int(20) unsigned NOT NULL default '0', `bytes_out` int(20) unsigned NOT NULL default '0', `timestamp` timestamp(14) NOT NULL, `host` varchar(128) NOT NULL default '', `id` bigint(20) unsigned NOT NULL auto_increment, PRIMARY KEY (`id`) ) TYPE=InnoDB
If you have tables of hosts and virtual hosts, you'll probably want to
normalize the database and modify AccountingQueryFmt to use the foreign key to
the vhost. For example,AccountingQueryFmt "INSERT INTO vhost_accounting (bytes_in, bytes_out, virtual_host_id) SELECT %r, %s, id FROM vhost WHERE virtual_host_name = LOWER('%h');"
There are a couple issues that make deploying mod_accounting to multiple machines a bit tricky. Each apache process does its own logging, and thus, maintains its own connection to the database server. If you have hundreds of Apache processes and multiple servers, you'll quickly have thousands of open connections to the database. To reduce the number of concurrent connections, I made a one-line change to mod_accounting to close the connection to MySQL after writing to the database so a new connection is used each time data is written to the database.
The next problem is simply the large amount of data that is collected. It
quickly grows to the point where generating reports in real-time is too slow.
To solve this problem, I created a summary table, vhost_accounting_summary, to
consolidate the data produced by each Apache process. I also decided that
hourly statistics were sufficient for my needs.
The records in the vhost_accounting table are removed once the data has been
inserted into vhost_accounting_summary. One other table,
vhost_accounting_timestamp, is used to keep track of when the summary table was
last updated and to ensure that data from vhost_accounting does not
accidentally get written to vhost_accounting_summary multiple times. (More
details below.)
CREATE TABLE `vhost_accounting_summary` ( `virtual_host` varchar(128) NOT NULL default '', `bytes_in` int(20) unsigned NOT NULL default '0', `bytes_out` int(20) unsigned NOT NULL default '0', `timestamp` timestamp(14) NOT NULL, `host` varchar(128) NOT NULL default '', KEY `virtual_host` (`virtual_host`), KEY `idx_timestamp_host` (`timestamp`,`host`) ) TYPE=InnoDB CREATE TABLE `vhost_accounting_timestamp` ( `timestamp` timestamp(14) NOT NULL ) TYPE=InnoDB
And here's the SQL to update the vhost_accounting_summary table.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT timestamp FROM vhost_accounting_timestamp FOR UPDATE;
SELECT @max_id:= max(id) FROM vhost_accounting;
INSERT INTO
vhost_accounting_summary
SELECT
virtual_host,
SUM(bytes_in),
SUM(bytes_out),
DATE_FORMAT(timestamp, '%Y-%m-%d %H:00:00') AS dayhour,
host
FROM
vhost_accounting
WHERE
id < @max_id
GROUP BY
virtual_host,
dayhour;
DELETE FROM vhost_accounting WHERE id < @max_id;
UPDATE vhost_accounting_timestamp SET timestamp = now();
COMMIT;
There are a couple important points to make about the SQL above. We want to
make sure that mod_accounting never has to wait to write to the
vhost_accounting because of a lock. In addition, we want to ensure that, if
multiple copies of the update script get run concurrently, it does not cause
the same data to be written to vhost_accounting_summary multiple times.
Mod_accounting does its work while processing a request. If mod_accounting is waiting for a lock, the Apache process will not finish processing the current request until the lock is removed or a lock timeout occurs. This can quickly cause all of the Apache children to be tied up and prevent new requests from being processed.
To prevent a lock from being set, we only act on records with a primary key one less than the maximum. This prevents a next-key lock from being set when we delete the records we have just processed.
To prevent multiple instances of the script from causing redundant data to be
stored, we do a locking read of the timestamp by using FOR
UPDATE. This lock only affects this summary script since
mod_accounting does not use the vhost_accounting_timestamp table.
Furthermore, we must use the READ-COMMITTED
isolation level to guarantee that once the script receives the lock, it
doesn't read any rows from vhost_accounting that have already been
deleted.
And finally, we're ready to generate some reports
SELECT SUM(bytes_in) AS bytes_in, SUM(bytes_out) AS bytes_out, SUM(bytes_in + bytes_out) AS total, virtual_host, host FROM vhost_accounting_summary WHERE timestamp >= '2004-11-18 00:00:00' AND timestamp <= '2004-11-18 23:59:59' GROUP BY virtual_host ORDER BY total DESC
I spent a few hours today researching Exchange replacements. These are products that are designed to replace Microsoft Exchange on the server, but still allow use of Outlook as a client, including the much-beloved calendaring features.
Here's what I came up with.
Date: Tue, 16 Nov 2004 16:24:55 -0800 From: "Christian G. Warden" <cwarden@zerolag.com> Subject: Exchange replacement analysis - phase 1 There are a handful of products that claim to be Exchange replacements. They all work in the same manner, using a custom MAPI connector, which is basically a plug-in for Outlook, to access the server. Each version of Outlook has different features so most of these products only work with certain versions of Outlook. Because I'm not very familiar with Outlook, it is difficult for me to tell if these products fully support the features of Outlook. We'll need to setup a test environment to fully evaluate any of these products. OpenGroupware[1] This was previously a closed source server that was open sourced a couple years ago. I evaluated it briefly a year or so ago, and it seemed stable and featureful, but had a bit of a clunky web interface. It looks like development is pretty active, though. I haven't evaluated the Outlook connector, ZideLook[2], which is a commercial product which costs about $50 per client. There is no demo of ZideLook available. ZideLook communicates with OpenGroupware using WebDAV. OpenGroupware just handles the groupware functionality and integrates with third-party IMAP servers. 1. http://www.opengroupware.org/en/index.html 2. http://esd.element5.com/product.html?cart=1&productid=517934&languageid=1&nolselection=1¤cies=EUR SUSE LINUX Openexchange Server[3] This is a commercial product. It is distributed a full linux distribution and cannot be installed on an existing Linux system. (Such an installation would not be supported at least.) Pricing is unclear. The product is supposed to be available for purchase online at novell.com, but isn't, perhaps because they are currently integrating the product with Novell's Groupwise. There is an online demo[4] and the Outlook connector is available for download[5]. Openexchange is made up of a number of open source components and comFire, the groupware component, which was licensed from a company called Netline. comFire has recently been open sourced by Netline as Open-Xchange[6], but the Outlook connector is not licensed for use with Open-Xchange. The Outlook connector communicates with the server using WebDAV. There is a good article about Openexchange[7]. 3. http://www.suse.com/us/business/products/openexchange/index.html 4. http://www.suse.com/us/business/products/openexchange/demo.html 5. http://www.suse.com/us/business/products/openexchange/download.html 6. http://mirror.open-xchange.org/ox/EN/product/ 7. http://www.linux-magazine.com/issue/48/Suse_Linux_Openexchange_41.pdf Bynari Insight Server[8] and Insight Connector[9] I believe Bynari was the first company with an "Exchange replacement on Linux" product. Their Outlook connector allows calendars and address books on an IMAP server. It claims to require the Insight Server, though Insight Server uses Cyrus as the IMAP server, so it may work with a normal Cyrus server. Insight Server is composed of a number of open source products such as Postfix, OpenLDAP, and Apache. Bynari seems to think most of the value is in the Connector since a 1000 user license for Insight Connector is $17,000, and a 1000 user license for a bundled Insight Server and Insight Connector is $18,000. (Insight Server without the Connector is also sold for $1,000.) A demo is available. 8. http://www.bynari.net/index.php?id=1169 9. http://www.bynari.net/index.php?id=7 BILL Workgroup Server[10]/Exchange4Linux[11] Documentation is kind of spotty on this one. I don't think it's worth evaluating except as a last resort. 10. http://www.billworkgroup.org/billworkgroup/home 11. http://www.exchange4linux.com/exchange4linux/Home None of the Above (IMAP/LDAP/SMTP/WebDAV or FTP) Depending on the customer's needs, perhaps Outlook in "Internet Mail Mode" will be sufficient. IMAP supports shared folders, but I don't know if it supports setting ACLs. Outlook also supports LDAP for address books, but I don't know if supports updating the directory. Outlook can send meeting requests and responses over email and publish free/busy time over FTP (and, I think, either WebDAV or HTTP PUT), but I don't know if this would meet the customer's needs. I recommend trying out Openexchange first as it seems to be the most open and widely deployed. Christian
Comments from anyone who has deployed one of these products for use with Outlook would be appreciated.
tech » mail | Permanent Link
Henrik Joretag tweeted that he doesn't like promises in javascript.
I’m just gonna say it:
I really don’t like promises in JS.
It’s a clever pattern, but I just don’t see the upside, sorry.
— Henrik Joreteg (@HenrikJoreteg) October 26, 2014I've been working on a large javascript application for the past two years, and I've found promises to be invaluable in expressing the dependencies between functions.
But Henrik's a smart guy, having authored Human Javascript and written lots of Ampersand.js, so I decided to look at a recent case where I found the use of promises helpful, and try to come up with the best alternative solution.
I recently discovered a bug in our app initialization. It's an offline-first mobile app. We have a function that looks like this:
App.init = function() {
return Session.initialize()
.then(App.initializeModelsAndControllers)
.then(App.manageAppCache)
.then(App.showHomeScreen)
.then(App.scheduleSync)
.then(_.noop, handleError);
};
When the app starts up, it needs to:
The chaining of promise-returning functions in App.init nicely expresses the
dependencies between these steps.
We need valid session info for the user before we can initialize the models,
which may require fetching data over the network.
We can't show the initial home screen until the controllers have been
initialized to bind to DOM events and display information from the models.
If there's a new version of the app available, we want to prompt the user to
restart before they start using the app, and we don't want to schedule a sync
until the app is being used.
Unfortunately, during a recent upgrade some users ran into a bug in some code I
wrote to upgrade the local database, called from
App.initializeModelsAndControllers. The result was an uncaught javascript
error, Uncaught RangeError: Maximum call stack size exceeded, on
the initialization screen which is displayed before App.init is called.
I fixed the bug which broke upgrading some databases, but I also realized that the dependency between App.initializeModelsAndControllers and App.manageAppCache should be reversed. If there's an upgrade available, we want users to be able to restart into it before initializing the app further (and running more code with a larger surface area for possible bugs).
The diff to make this change with our promise-returning functions is nice and simple:
--- a/app.js
+++ b/app.js
@@ -1,7 +1,7 @@
App.init = function() {
return Session.initialize()
- .then(App.initializeModelsAndControllers)
.then(App.manageAppCache)
+ .then(App.initializeModelsAndControllers)
.then(App.showHomeScreen)
.then(App.scheduleSync)
.then(_.identity, handleError);
(The careful reader will notice that stuck users need a way of upgrading the
broken app stored in the app cache. The cache manifest gets fetched
automatically to check for and trigger an app update each time the page is
loaded, so even though we haven't set up an event handler to prompt the user to
restart after the update has been downloaded, if the user waits a few minutes
to ensure that the app update is complete, then kills and restarts the app, the
new version will be loaded.
The careful reader who happens to be a coworker or affected customer will note
that this didn't actually work because our cache manifest requires a valid
session and our app starts up without refreshing the OAuth session to speed up
start-up time. An upcoming release of GreatVines
app to the iOS app store will contain a new setting to trigger the session
refresh at app initialization. Affected customers have been given a
work-around that requires tethering the device to a computer via USB.
Developers using the Salesforce Mobile SDK are welcome to email me for more
info.)
App.init = function() {
return Session.initialize()
.then(App.initializeModelsAndControllers)
.then(App.manageAppCache)
.then(App.showHomeScreen)
.then(App.scheduleSync)
.then(_.noop, handleError);
};
Now, back to the issue at hand. What would this function look like using an alternative to promises. Ignoring the possible use of a preprocessor that changes the semantics of javascript, the two likely candidates are callback passing and event emitter models.
The typical callback hell approach looks something like this:
App.init = function(success, error) {
Session.initialize(function() {
App.manageAppCache(function() {
App.initializeModelsAndControllers(function() {
App.showHomeScreen(function() {
App.scheduleSync(success, error);
}, error);
}, error);
}, error);
}, error);
};
For simplicity, I've ignored the fact that the original version both had some error-handling and returns the error to the caller through a rejected promise, but I'm pretty sure this is not what Henrik had in mind. So what might an improved callback-passing approach look like?
App.init = function(success, error) {
var scheduleSync = App.scheduleSync.bind(App, success, error);
var showHomeScreen = App.showHomeScreen.bind(App, scheduleSync, error);
var initializeModelsAndControllers = App.initializeModelsAndControllers.bind(App, showHomeScreen, error);
var manageAppCache = App.manageAppCache.bind(App, initializeModelsAndControllers, error);
Session.initialize(manageAppCache, error);
};
By pre-binding the callbacks, we avoid the nested indentations, but we've reversed the order of the dependencies, so that's not a great improvement.
I've failed to come up with a good callback-passing alternative to promises, so let's look at what event emitter approach would look like.
App.init = function() {
BackboneEvents.mixin(App);
App.once('sessionInitialized', App.manageAppCache);
App.once('appCacheManaged', App.initializeModelsAndControllers);
App.once('modelsAndControllersInitialized', App.showHomeScreen);
App.once('homeScreenShown', App.scheduleSync);
var initialized = App.trigger.bind(App, 'initialized');
App.once('syncScheduled', initialied);
App.once('sessionInitializationError appCacheManagementError modelsAndControllersInitializationError homeScreenError syncSchedulingError', function(error) {
App.off('sessionInitialized', App.manageAppCache);
App.off('appCacheManaged', App.initializeModelsAndControllers);
App.off('modelsAndControllersInitialized', App.showHomeScreen);
App.off('homeScreenShown', App.scheduleSync);
App.off('syncScheduled', initialied);
App.trigger('initializationError', error);
});
Session.initialize();
};
This approach does allow us to list dependencies from top to bottom, and
doesn't take us into calllback hell, but it has some problems as well.
With the promise-returning and callback-passing approaches, each function was
loosly coupled to the caller, returning a promise or accepting success and
error callbacks. With this event emitter approach, we need to make sure both
the caller and called agree upon the names of events. Admittedly, most
functions are going to be more tightly coupled than these, regardless of the
approach, having to agree upon parameters to the called function and parameters
to the resolved promise or callback, but they don't need to share any special
information like the event name to determine when the called function is done.
This event emitter example also requires more work to clean up when an error
occurs.
Perhaps instead of using a single event bus, we could listen for events on multiple objects and standardize on event names. Events could be triggered on each function.
App.init = function() {
var initialized = App.init.trigger.bind(App.init, 'initialized');
var error = function(error) {
App.init.trigger('error', error);
Session.initialize.off('done', App.manageAppCache);
Session.initialize.off('error', error);
App.manageAppCache.off('done', App.initializeModelsAndControllers);
App.manageAppCache.off('error', error);
App.initializeModelsAndControllers.off('done', App.showHomeScreen);
App.initializeModelsAndControllers.off('error', error);
App.homeScreenShown.off('done', App.scheduleSync);
App.homeScreenShown.off('error', error);
App.scheduleSync.off('done', initialized);
App.scheduleSync.off('error', error);
};
// Assume every function mixes in BackboneEvents
Session.initialize.once('done', App.manageAppCache);
Session.initialize.once('error', error);
App.manageAppCache.once('done', App.initializeModelsAndControllers);
App.manageAppCache.once('error', error);
App.initializeModelsAndControllers.once('done', App.showHomeScreen);
App.initializeModelsAndControllers.once('error', error);
App.homeScreenShown.once('done', App.scheduleSync);
App.homeScreenShown.once('error', error);
App.scheduleSync.once('done', initialized);
App.scheduleSync.once('error', error);
Session.initialize();
};
This is very verbose, containing a lot of boilerplate. We could create a function to create these bindings, e.g.
var when = function(fn, next, error) {
BackboneEvents.mixin(fn);
fn.once('done', next);
fn.once('error', error);
};
when(Session.initialize, App.manageAppCache, error);
...
But it doesn't feel like we're on the right path. We'd still have to handle
unbinding all of the error handlers when an error occurs in one of the
functions.
It's also worth pointing out that using an event emitter isn't really
appropriate in a case like this where you want to get an async response to a specific
function call. If the called function can be called multiple times, you don't
know that the event that was triggered was in response to your call without
passing additional information back with the event, and then filtering.
Unsatisfied with any of the alternatives to promises I came up with, I replied
to Henrik's tweet, and he said that he uses Async.js and Node's error-first
convention. So let's look at what using async.series would
look like.
App.init = function(callback) {
async.series([
Session.initialize,
App.manageAppCache,
App.initializeModelsAndControllers,
App.showHomeScreen,
App.scheduleSync,
], callback);
Finally, we've come to an alternative solution that feels equivalent to the
promise one. The async.series solution has at least one benefit
over the promise-chaining one: the Async library has a separate function,
async.waterfall, to use when the return value from one function
should be passed as a parameter to the next function in the series. In my
original solution, it's not clear without looking at the function definitions
whether the return value from each function is used by the next function.
Given the simple error handling in App.init, in which an error anywhere in the chain aborts the flow, and a single error handler function is responsible for handling the error from any of the functions, Async.js would be a decent replacement.
But for a more complicated flow, a promise-based approach would
probably come out on top. For example, here's what
App.initializeModelsAndControllers looks like:
App.initializeModelsAndControllers = function() {
return Base.init()
.then(Base.sufficientlyLoadedLocally)
.then(App.handleUpgrades, App.remoteInit)
.then(App.loadControllers);
};
This function introduces a fork in the chain of serialized functions:
It looks like this could be done with async.auto,
but it would probably start getting messy.
We haven't had much luck with Google Ads for Postica so far. There's a lot of competition in the spam filtering market so it's hard to stand out among 10 ads for spam/virus filtering products and services on the same page.
It's frustrating having what I think is a great service, and not being able to reach potential customers. We're mostly focusing on selling through partners now, but we haven't completely given up on Google Ads yet. Here's our latest.
business » marketing » advertising | Permanent Link
I've updated my screen-scraped rss feed
of Northern
Trust's Economic Research to include their International Comments.
Also, I don't think I mentioned it when I first set it up, but I also have a
feed
of
the Prudent Bear news.
I've figured out why I find sales so unpleasant. Being a business owner requires a much different mind-set than that of a typical techie.
I've been working as a sys admin and web developer for the past seven years. As such, I only have to sell myself to an employer or client once, and typically, over a single one or two hour interview period. Having to sell a product or service to a customer who hasn't already "hired" you is a much different proposition.
As a techie, I don't often have to deal with failure. When working on a new project or fixing an existing system that is broken, I just keep working on it until it's done. Rarely must I concede defeat and give up hope of finding a solution.
Failure is a normal part of sales, though. Salespeople cannot always just keep working on a sales prospect until a deal is closed. This would likely result in restraining orders.
With Postica, we decided that Greg would focus on sales, while I would handle technical issues. Nonetheless, I participate in some sales calls, and I become frustrated when sales don't increase as quickly as I would like.
Direct sales requires a lot of repetition. You generally have to give the same pitch over and over to each new potential customer. This conflicts with my natural desire to automate repetitive behavior. When I come across a task that I have to do repeatedly, I usually write a script or subroutine to automate the process.
Part of marketing is to automate the sales message, but when bootstrapping a business with limited capital, much of your marketing message must be delivered personally to potential customers.
I need to find some good books on sales that explains how to get into the correct mind-set.
Michael Cage discusses the need to train non-salespeople for sales. In order to do this, the non-salespeople must learn to think like salespeople.
A couple years ago, I really got into blackjack. To be successful at blackjack requires basically becoming a computer. You must keep track of the count, and bet and play exactly as the rules (based on calculated odds) that you've memorized tell you that must. You must not become emotional after losing a lot of money when doubling after splitting (though you may want to feign being upset for the pit boss's benefit). What happens during a single hand is irrelevant. Your goal is to grind out a profit by having a slight advantage over the house based on the rules and your ability to vary play based on the count.
A fantastic book on blackjack is Million Dollar Blackjack. It describes how Ken Uston and his team won $4,000,000, persevering through lawsuits, pit bosses, the gambling commission, and malfunctioning electronic equipment (they used some computerized counting gear). Business tales are often similar. For example, Joe Kraus's story about how Excite got the deal to be featured in Netscape Navigator is a story about persistence in the face of rejection.
Is there a way to reconcile the techie mind-set with a salesperson's world?
I sent the following email to the Motorola Complaint department this morning. It seems I'm not the only one who has suffered from Motorola's decision to completely isolate their customer service department from those in the company who do more than simply answer the phone. Pat Keene has set up a blog called Hello Moto. Where's my phone? detailing roughly the same experience I describe below. If your call center is in India, and your repair center is in Tennessee, it's not like the CSR can walk over to the shipping department or a technician to get an actual answer. I got the idea to copy Stu Reed from Pat.
Date: Fri, 7 Sep 2007 10:55:32 -0700 From: "Christian G. Warden"To: d2d001@motorola.com Cc: stu.reed@motorola.com Subject: Customer Service Breakdown (RMA #XXXXXXXXXX) Sir or Madam, I sent my Moto Q in for repair at the end of July with RMA #xxxxxxxxxx (zip code xxxxx). It was received by Motorola on July 31. According to the IDEN website, the replacement was sent back to me on August 17. The Fedex web site confirms that it received notification of a package shipped Overnight Priority with tracking number xxxxxxxxxxxx, but has no record of actually receiving the package. Having not received the package after one week, I called customer service at 1-800-453-0920 on Friday, August 24. I was given case number xxxxxxxx, and was told that I would receive a call back within 24 hours from the repair facility. I didn't receive a call until 3 days later, on Monday, August 27, and I was told that it was a "courtesy call", that the caller didn't have any information about my phone, and that I would receive a call within 24 hours. I never received that call. I have since called the customer service number four or five more times. Every time, I am told that I will receive a call within 24 hours, but I have never received another call since the courtesy call. During one call, I was transfered to the voice mail at repair facility. I left a message, but alas, did not receive a call back. On September 1, I submitted a request through your web site (Incident #xxxxxx-xxxxxx) explaining my situation and my inability to get through to someone regarding the whereabouts of my phone. I received an email response saying that I would receive a call back. I didn't. On Tuesday, September 4, I called 1-800-331-6456 as suggested in the response to by online request. I was transfered to same call center I had spoken with on numerous occasions prior. I explained the situation and was given a new case number, xxxxxxxx, and once again told that I would receive a call within 24 hours. I didn't receive that call. On September 5, I received an automated response that my online request had been closed as "SOLVED". I responded that it was indeed not solved, and that I still needed someone to contact me. I received an email response, "Once again, we apologize for any inconvenience this delay may cause you. You will be contacted back to the contact phone number you provided for accurate assistance with your repair." This morning I called customer service once again, explained the situation, and was told that they would contact the repair facility to have them contact me. I explained that this process obviously does not work because the repair facility never calls me. I asked to speak to someone at the repair facility, and was told that no phone number was available. I asked to speak to the person responsible to managing the repair facility, and was told that no such person was available. I asked for the phone number of the Chief Operations Officer, or a person of a similar role in your organization, and was told that such information was not available. Finally, I simply asked to speak with the supervisor in the call center. I was told that the supervisor, whose name the CSR ("Nicole", #351359) refused to give me, was preoccupied and would call me within 10 minutes. I didn't receive that call either. Due to the malfunctioning phone, and inability to get it replaced in a timely manner, I have been without a phone for about six weeks, and have continued to incur charges from my carrier, Sprint, during this period. Your assistance in resolving this issue would be greatly appreciated. Regards, Christian Warden (temporary) mobile: 310.xxx.xxxx home: 408.xxx.xxxx
Update (09/23/2007): After I sent the email above, I received a call from Dorothy Hodges in the consumer advocacy department. She was very pleasant and eager to resolve my problem. She ordered me another Q, but they weren't in stock so she sent me a RAZR2 as a loaner. She also sent me a bluetooth headset as a sorry-we-screwed-up consolation token.
Upon receiving the RAZR, I checked my voicemail, and I had two messages from the management of my apartment complex. It turns out the original replacement phone was devlivered, but that Fedex didn't leave a notice, and that the tracking number Motorola recorded was incorrect. So last Monday I called Sprint with Dorothy and activated the Q. She requested a credit on my behalf since I had been without a phone for six weeks while still paying the bill. I managed to get them from their original offer of 100 minutes to $10 to $30, at which point I accepted.

The state is that great fiction by which everyone tries to live at the expense of everyone else. - Frederic Bastiat